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"
}
| Field | Required | Notes |
|---|---|---|
username | yes | 3–32 chars. The profile handle — becomes the URL opendocs.cc/@<username>. Must pass the username rules. |
workspaceName | no | 2–120 chars. Omit when the user is accepting an invitation to an existing workspace. |
workspaceSlug | no | 3–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"
}
}
apiKeyis 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,apiKeyisnull.- The auto-created key is named
"Default"and can be revoked and replaced like any other — see API keys.
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
| Status | error | When |
|---|---|---|
400 | onboarding_failed | Anything the service layer rejects — username taken, slug taken, slug reserved, validation failure, etc. The message carries the specifics. |
401 | unauthorized | No 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"
}
| Field | Type | Notes |
|---|---|---|
fullName | string or null | ≤ 120 chars. Pass null to clear. |
profilePictureUrl | string 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
| Status | error | When |
|---|---|---|
400 | missing_file | No file in the multipart body |
400 | unsupported_mime | Not one of JPEG/PNG/WebP/GIF/SVG |
413 | file_too_large | File exceeds 2 MB |
503 | storage_not_configured | Object 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"
}
| Field | Required | Notes |
|---|---|---|
contentType | yes | 1–128 chars. Must be an image MIME (JPEG, PNG, WebP, GIF, SVG). |
filename | no | ≤ 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:
POST /storage/images/presign-uploadwith the image's MIME type.PUT uploadUrlwith the image bytes and a matchingContent-Typeheader. No auth needed on the PUT — the URL is signed.- Reference
publicUrlin your Markdown (). POST /publishwith the updated Markdown.
The presigned URL expires in 5 minutes. Re-request if you hit an expired signature.
Errors
| Status | error | When |
|---|---|---|
400 | unsupported_mime | contentType wasn't an image MIME |
400 | no_workspace | Caller hasn't completed onboarding |
400 | invalid_request | Missing contentType or wrong types |
503 | storage_not_configured | Object storage isn't set up (self-hosted only) |
Typical first-run sequence
For a brand-new user, the dashboard calls these in order:
POST /api/v1/onboarding— claim username, create workspace, get API key.GET /api/v1/me— read the workspace + quota for the UI.POST /api/v1/profile/avatar/upload— upload an avatar (optional).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.
Related pages
- Authentication — how the session vs. API-key split works
- API keys — create and revoke keys after onboarding
- Usernames and workspace slugs — why usernames and workspace slugs are different things
- Workspaces API — settings for the workspace created by onboarding