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.
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
| 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-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
| 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
{ "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
{ "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).
{ "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
| Status | error | When |
|---|---|---|
400 | missing_file | No file 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.
{ "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. 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.
{ "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:
| Status | error | Meaning |
|---|---|---|
400 | no_workspace | Caller hasn't completed onboarding yet |
400 | invalid_request | Body failed schema validation |
401 | unauthorized | Missing/expired session — API keys don't work here |
403 | forbidden | Caller isn't owner/admin for this mutation |
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