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",
"name": "Default",
"plaintext": "od_live_r7t9QxKm8zBvN3wY2hLp4dFaS1cE6uJo",
"prefix": "od_live_",
"last4": "6uJo"
}
}
  • 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": {
"userId": "usr_1",
"username": "ada",
"fullName": "Ada Lovelace",
"email": "ada@acme.com",
"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

{
"contentType": "image/png",
"filename": "architecture-diagram.png"
}
FieldRequiredNotes
contentTypeyes1–128 chars. Must be an image MIME (JPEG, PNG, WebP, GIF, SVG).
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
}

Workflow:

  1. POST /storage/images/presign-upload with the image's MIME type.
  2. PUT uploadUrl with the image bytes and a matching Content-Type 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
400unsupported_mimecontentType wasn't an image MIME
400no_workspaceCaller hasn't completed onboarding
400invalid_requestMissing contentType or wrong types
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.