Skip to main content

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.

Mostly session auth

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

StatuserrorWhen
401unauthorizedNo 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

ParamRequiredDescription
workspaceIdnoWhich 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

ParamRequiredDescription
slugyesThe 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

StatuserrorWhen
400invalid_slugSlug failed format validation
400reserved_slugSlug matches a reserved route name
409slug_takenSlug is already in use by another workspace
403plan_limitUser 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

StatuserrorWhen
400invalid_slugFormat validation failed
400reserved_slugMatches a reserved route name
409slug_takenAlready in use
403forbiddenCaller 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.

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

StatuserrorWhen
400missing_fileNo file in the multipart body
400missing_workspace_idNo workspaceId form field in the multipart body
400unsupported_mimeNot one of JPEG/PNG/WebP/GIF/SVG
403forbiddenCaller isn't owner/admin
413file_too_largeFile exceeds 2 MB
503storage_not_configuredObject 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. Pass null or omit to fall back to the converter default.
  • linkUrl: a URL. acme.com is accepted — the API auto-prefixes https:// 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:

StatuserrorMeaning
400invalid_requestBody failed schema validation — commonly a missing or empty workspaceId
400missing_workspace_idMultipart/query-string routes only: workspaceId form field or query param not supplied
401unauthorizedMissing/expired session — API keys don't work here
403forbiddenCaller isn't owner/admin for this mutation, or not a member of the workspace at all
404not_foundDomain ID (or equivalent) doesn't belong to this workspace

See Errors and pagination for the complete error catalogue.