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.

Session auth only

Every endpoint on this page requires a browser session cookie. API keys return 401 unauthorized — workspace settings are a dashboard surface. 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",
"domains": [
{ "id": "dom_1", "domain": "docs.acme.com", "status": "verified" }
]
}

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/workspace

Returns the current user's workspace settings.

{
"workspace": { "...": "see shape above" }
}

POST /api/v1/workspace

Rename the workspace. Owner/admin only — members get 403 forbidden.

Request body

{ "name": "Acme Inc." }

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-plan workspace cap (Free: 1, Pro: 2, 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

{ "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

{ "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).

{ "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
  • 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
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.

{ "show": true }

POST /api/v1/workspace/bio

Short workspace description shown on the public profile page.

{ "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.

{ "font": "Inter" }

Pass null 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

{
"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.

{ "allowed": true }

Owner/admin only.

Custom domains

Pro and Team workspaces can attach a custom domain so published docs resolve at docs.your-company.com instead of opendocs.cc/<slug>.

POST /api/v1/workspace/domains

Add a domain. Verification happens out-of-band via DNS.

{ "domain": "docs.acme.com" }

Returns 201 with the domain record:

{
"id": "dom_1",
"domain": "docs.acme.com",
"status": "pending_verification"
}

Status progresses to verified once the required DNS record is observed.

DELETE /api/v1/workspace/domains/:domainId

Remove a domain.

{ "success": true }

Returns 404 not_found if the domain doesn't belong to this workspace.

Common error codes

Across this page:

StatuserrorMeaning
400no_workspaceCaller hasn't completed onboarding yet
400invalid_requestBody failed schema validation
401unauthorizedMissing/expired session — API keys don't work here
403forbiddenCaller isn't owner/admin for this mutation
404not_foundDomain ID (or equivalent) doesn't belong to this workspace

See Errors and pagination for the complete error catalogue.