Workspaces API
The workspace is the sharing boundary and URL prefix for every published
post — opendocs.cc/<workspace-slug>/<post-slug>. These endpoints cover
everything about a workspace except team and invitation management,
which lives on its own page.
All mutating endpoints on this page require a browser session cookie.
API keys return 401 unauthorized on them — workspace settings are a
dashboard surface. The one exception is
GET /api/v1/workspaces below, which accepts
either session or API-key auth so the CLI's workspace switcher works.
See Authentication for the distinction.
All routes fall under the general 120 req/min rate bucket.
The workspace record
Most of these endpoints return the updated workspace. The shape is:
{
"id": "ws_123",
"name": "Acme",
"slug": "acme",
"bio": "Internal docs for Acme.",
"brandColor": "#3366ff",
"logoUrl": "https://cdn.opendocs.cc/logos/ws_123.png",
"showLogoInExports": true,
"allowPublicDocuments": true,
"exportFont": "Inter",
"exportFooter": {
"enabled": true,
"companyName": "Acme",
"tagline": "Ship fast. Document faster.",
"linkUrl": "https://acme.com",
"linkLabel": "acme.com"
},
"plan": "pro",
"role": "owner"
}
Fields may be null when unset. role is the caller's role in the
workspace (owner / admin / member) — the API uses this to decide whether
a mutation is allowed.
GET /api/v1/workspaces
Lists every workspace the caller is an active member of. Dual auth —
works with either session cookie or API key. The CLI hits this on every
opendocs workspace list and opendocs workspace use <slug>, and the
dashboard uses it to render the workspace switcher.
Distinct from GET /api/v1/workspace (singular —
settings for the caller's current workspace) and
GET /api/v1/workspaces/current (the one
workspace surfaced during onboarding).
Response
{
"workspaces": [
{
"id": "ws_123",
"name": "Acme",
"slug": "acme",
"role": "owner"
},
{
"id": "ws_456",
"name": "Side Project",
"slug": "side-project",
"role": "member"
}
]
}
Ordered by joinedAt ascending — the workspace the user joined first
comes out on top. role is one of owner, admin, member. Only
active memberships are returned; invited-but-not-yet-accepted and
disabled memberships are filtered out.
Errors
| Status | error | When |
|---|---|---|
401 | unauthorized | No valid session cookie or API key |
No 404 — an authenticated user with zero active memberships returns
{ "workspaces": [] }.
GET /api/v1/workspace
Returns the caller's workspace settings.
Query parameters
| Param | Required | Description |
|---|---|---|
workspaceId | no | Which workspace to load. If omitted, the caller's first active membership is returned — fine for single-workspace users, but multi-workspace users should always pass an explicit id. |
{
"workspace": { "...": "see shape above" }
}
POST /api/v1/workspace
Rename the workspace. Owner/admin only — members get 403 forbidden.
Request body
{ "workspaceId": "ws_123", "name": "Acme Inc." }
workspaceId is required on every workspace mutation below — the server
does not fall back to the caller's "first" workspace. name must be
2–120 characters.
Success response
{ "id": "ws_123", "name": "Acme Inc.", "slug": "acme", "...": "..." }
GET /api/v1/workspaces/current
Returns the onboarding-shaped view of the caller's workspace — the same
workspace object the onboarding flow reads to decide whether to prompt
for a name and slug.
{
"workspace": {
"id": "ws_123",
"name": "Acme",
"slug": "acme"
}
}
Used internally by the dashboard; API-key callers should prefer
GET /api/v1/me for a richer response.
GET /api/v1/workspace/check-slug
Quick availability check for the workspace-slug picker. Used during onboarding and from the rename dialog.
Query parameters
| Param | Required | Description |
|---|---|---|
slug | yes | The candidate slug (3–32 chars, lowercase, a-z 0-9 _ -). |
Response
{ "available": true }
Or, when taken / reserved / malformed:
{ "available": false, "reason": "That workspace ID is already taken." }
The reason string is safe to render verbatim in a form helper.
POST /api/v1/workspaces
Create a new workspace. Gated by the per-account workspace cap (Free: 1, Pro: 2 plus paid extra workspace add-ons, Team: unlimited — see Plans and limits).
Request body
{ "name": "Acme Labs", "slug": "acme-labs" }
Success response
Status 201.
{ "id": "ws_456", "name": "Acme Labs", "slug": "acme-labs", "...": "..." }
Errors
| Status | error | When |
|---|---|---|
400 | invalid_slug | Slug failed format validation |
400 | reserved_slug | Slug matches a reserved route name |
409 | slug_taken | Slug is already in use by another workspace |
403 | plan_limit | User is at their per-plan workspace cap |
POST /api/v1/workspace/slug
Rename the workspace slug. Owner/admin only. Every existing post URL
under /<old-slug>/… stops resolving — old slugs are not redirected. The
dashboard surfaces this with a confirmation dialog before calling.
Request body
{ "workspaceId": "ws_123", "slug": "acme-new" }
Success response
{ "slug": "acme-new" }
If the caller submits the slug they already have, the response is the
same shape plus "unchanged": true with status 200 — callers can treat
this as a successful no-op.
Errors
| Status | error | When |
|---|---|---|
400 | invalid_slug | Format validation failed |
400 | reserved_slug | Matches a reserved route name |
409 | slug_taken | Already in use |
403 | forbidden | Caller isn't owner/admin |
Branding
POST /api/v1/workspace/brand-color
{ "workspaceId": "ws_123", "color": "#3366ff" }
Must be a 6-digit hex string starting with #. Owner/admin only.
POST /api/v1/workspace/logo
Set the logo URL directly (e.g., to a URL you already host).
{ "workspaceId": "ws_123", "logoUrl": "https://cdn.acme.com/logo.png" }
Pass "logoUrl": null to clear the logo. Owner/admin only.
POST /api/v1/workspace/logo/upload
Multipart upload. Pushes the file to OpenDocs' object store, then writes
the resulting public URL into the workspace's logoUrl.
- Form field:
file(the image bytes) - Form field:
workspaceId(required — which workspace to attach the logo to) - Max size: 2 MB
- Accepted MIME types: JPEG, PNG, WebP, GIF, SVG
Success response
{
"logoUrl": "https://cdn.opendocs.cc/logos/ws_123.png",
"workspace": { "...": "see shape above" }
}
Errors
| Status | error | When |
|---|---|---|
400 | missing_file | No file in the multipart body |
400 | missing_workspace_id | No workspaceId form field in the multipart body |
400 | unsupported_mime | Not one of JPEG/PNG/WebP/GIF/SVG |
403 | forbidden | Caller isn't owner/admin |
413 | file_too_large | File exceeds 2 MB |
503 | storage_not_configured | Object storage isn't set up (self-hosted only) |
POST /api/v1/workspace/logo-in-exports
Toggle whether the workspace logo is embedded in PDF/DOCX exports.
{ "workspaceId": "ws_123", "show": true }
POST /api/v1/workspace/bio
Short workspace description shown on the public profile page.
{ "workspaceId": "ws_123", "bio": "Internal docs for the Acme platform team." }
bio must be ≤ 500 characters. Pass null to clear.
Export customization
POST /api/v1/workspace/export-font
Pick the font used when rendering PDF/DOCX exports.
{ "workspaceId": "ws_123", "font": "Inter" }
Pass null for font to fall back to the default (Arial). Allowed values:
- Sans-serif:
Arial,Inter,IBM Plex Sans,Lato - Serif:
Merriweather,Vollkorn
Unknown fonts return 400 invalid_font.
POST /api/v1/workspace/export-footer
Configure the footer line printed on exported documents.
Request body
{
"workspaceId": "ws_123",
"enabled": true,
"companyName": "Acme",
"tagline": "Ship fast. Document faster.",
"linkUrl": "acme.com",
"linkLabel": "acme.com"
}
enabled(required): turns the footer on or off.companyName,tagline,linkLabel: optional strings, max 120 chars each. Passnullor omit to fall back to the converter default.linkUrl: a URL.acme.comis accepted — the API auto-prefixeshttps://if no scheme is present.
Owner/admin only.
Public-documents toggle
POST /api/v1/workspace/public-documents
Workspace-wide kill-switch for public visibility. When allowed: false,
all existing public posts become inaccessible and new posts cannot be
published as public. Useful for compliance-sensitive workspaces that
never want to ship public URLs.
{ "workspaceId": "ws_123", "allowed": true }
Owner/admin only.
Common error codes
Across this page:
| Status | error | Meaning |
|---|---|---|
400 | invalid_request | Body failed schema validation — commonly a missing or empty workspaceId |
400 | missing_workspace_id | Multipart/query-string routes only: workspaceId form field or query param not supplied |
401 | unauthorized | Missing/expired session — API keys don't work here |
403 | forbidden | Caller isn't owner/admin for this mutation, or not a member of the workspace at all |
404 | not_found | Domain ID (or equivalent) doesn't belong to this workspace |
See Errors and pagination for the complete error catalogue.
Related pages
- Team and invitations API — the
other half of
workspace.ts - Usernames and workspace slugs — why slug renames matter
- Plans and limits — the workspace/member/document caps per tier
- Onboarding and profile — how the first workspace is created