Skip to main content

Onboarding and profile

The endpoints that wire up a fresh account: claim a username, create the first workspace, then manage profile fields and image uploads. These are grouped together because they're all one workflow — a signed-up user touches every one of them during their first few minutes.

Auth varies per endpoint; each is called out inline.

POST /api/v1/onboarding

Completes onboarding for a freshly signed-up user. Session auth.

Creates (or joins) the user's workspace and — when this is the first workspace — auto-creates the first API key so the CLI works immediately.

Request body

{
"username": "ada",
"workspaceName": "Acme",
"workspaceSlug": "acme"
}
FieldRequiredNotes
usernameyes3–32 chars. The profile handle — becomes the URL opendocs.cc/@<username>. Must pass the username rules.
workspaceNameno2–120 chars. Omit when the user is accepting an invitation to an existing workspace.
workspaceSlugno3–32 chars, lowercase, a-z 0-9 _ -. The URL prefix for every post — opendocs.cc/<slug>/<post-slug>. Same omission rule as workspaceName.

Success response

{
"user": { "id": "usr_1", "username": "ada" },
"workspace": {
"id": "ws_123",
"name": "Acme",
"slug": "acme",
"role": "owner"
},
"hasWorkspace": true,
"apiKey": {
"id": "key_ghi789",
"label": "Default",
"keyHint": "6uJo",
"plaintext": "od_live_r7t9QxKm8zBvN3wY2hLp4dFaS1cE6uJo",
"createdAt": "2026-04-17T10:15:00Z"
}
}
  • apiKey is only populated when a new workspace was created by this call. If the user already had a workspace, or if they're joining an existing one via invitation, apiKey is null.
  • The auto-created key is named "Default" and can be revoked and replaced like any other — see API keys.
Save the plaintext

If apiKey.plaintext is present, save it now. This is the only time the API returns the plaintext key — the dashboard and list endpoint only show the redacted form afterwards.

Errors

StatuserrorWhen
400onboarding_failedAnything the service layer rejects — username taken, slug taken, slug reserved, validation failure, etc. The message carries the specifics.
401unauthorizedNo session cookie

GET /api/v1/profile

Read the caller's profile. Dual auth — works with either session cookie or API key.

Response

{
"profile": {
"username": "ada",
"fullName": "Ada Lovelace",
"profilePictureUrl": "https://cdn.opendocs.cc/avatars/usr_1.png",
"bio": null
}
}

fullName, profilePictureUrl, and bio may be null when unset.

POST /api/v1/profile

Update profile fields. Session auth only.

Request body

Both fields are optional — omit a field to leave it unchanged.

{
"fullName": "Ada Lovelace",
"profilePictureUrl": "https://cdn.acme.com/ada.png"
}
FieldTypeNotes
fullNamestring or null≤ 120 chars. Pass null to clear.
profilePictureUrlstring or null≤ 2048 chars. Pass null to clear.

Success response

Same shape as GET /api/v1/profile, reflecting the updated state.

POST /api/v1/profile/avatar/upload

Multipart upload. Uploads to object storage and sets profilePictureUrl in one step. Session auth only.

  • Form field: file
  • Max size: 2 MB
  • Accepted MIME types: JPEG, PNG, WebP, GIF, SVG

Success response

{
"profilePictureUrl": "https://cdn.opendocs.cc/avatars/usr_1.png",
"profile": { "...": "see GET /profile shape" }
}

Errors

StatuserrorWhen
400missing_fileNo file in the multipart body
400unsupported_mimeNot one of JPEG/PNG/WebP/GIF/SVG
413file_too_largeFile exceeds 2 MB
503storage_not_configuredObject storage isn't set up (self-hosted only)

POST /api/v1/storage/images/presign-upload

Request a short-lived presigned PUT URL for uploading an image directly to object storage without streaming through the API. Dual auth — works with either session cookie or API key. Used by the CLI to push inline post images (screenshots, diagrams) before calling /publish.

Request body

{
"workspaceId": "ws_123",
"contentType": "image/png",
"contentLength": 348912,
"filename": "architecture-diagram.png"
}
FieldRequiredNotes
workspaceIdyesThe workspace the uploaded image will live under. Caller must be an active member.
contentTypeyes1–128 chars. Must be an image MIME (JPEG, PNG, WebP, GIF, SVG).
contentLengthyesImage size in bytes. Must be a positive integer up to 10 MB.
filenameno≤ 255 chars. Purely informational — used for logging and to preserve the extension when ambiguous.

Success response

{
"uploadUrl": "https://<storage-host>/…?X-Amz-Signature=…",
"publicUrl": "https://cdn.opendocs.cc/images/ws_123/abc123.png",
"key": "images/ws_123/abc123.png",
"expiresInSeconds": 300,
"maxBytes": 10485760
}

Workflow:

  1. POST /storage/images/presign-upload with the image's MIME type and byte size.
  2. PUT uploadUrl with the image bytes and a matching Content-Type and Content-Length header. No auth needed on the PUT — the URL is signed.
  3. Reference publicUrl in your Markdown (![…](<publicUrl>)).
  4. POST /publish with the updated Markdown.

The presigned URL expires in 5 minutes. Re-request if you hit an expired signature.

Errors

StatuserrorWhen
400invalid_requestMissing workspaceId / contentType, or wrong types
400unsupported_mimecontentType wasn't an image MIME
403forbiddenCaller isn't an active member of workspaceId
503storage_not_configuredObject storage isn't set up (self-hosted only)

Typical first-run sequence

For a brand-new user, the dashboard calls these in order:

  1. POST /api/v1/onboarding — claim username, create workspace, get API key.
  2. GET /api/v1/me — read the workspace + quota for the UI.
  3. POST /api/v1/profile/avatar/upload — upload an avatar (optional).
  4. POST /api/v1/workspace/logo/upload — upload the workspace logo (optional, owner/admin only — see Workspaces API).

From there the user is ready to publish via the CLI.