REST API Reference
DocPlatform exposes a RESTful JSON API. Business endpoints live under /api/v1/, while infrastructure endpoints (auth, health, git webhooks) use the unversioned /api/ prefix.
Base URLs
/api/v1/* — business endpoints (content, workspaces, search, admin)
/api/* — infrastructure endpoints (auth, health, git webhook, AI)
API Quickstart
Create a page
# 1. Create a new page
curl -X POST https://app.example.com/api/v1/content/{workspace_id}/guides/deployment \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"title": "Deployment Guide",
"body": "# Deployment\n\nFollow these steps to deploy DocPlatform.",
"tags": ["devops", "getting-started"],
"publish": false
}'
# Response: 201 Created
# If page already exists: 409 Conflict — use PUT to update
Update a page (read-modify-write)
Updating requires a three-step flow to prevent concurrent edit conflicts:
# 1. Read the page to get the current content_hash
curl https://app.example.com/api/v1/content/{workspace_id}/guides/deployment \
-H "Authorization: Bearer {token}"
# Response includes: "content_hash": "sha256:abc123..."
# 2. Modify the content locally, then PUT with the hash
curl -X PUT https://app.example.com/api/v1/content/{workspace_id}/guides/deployment \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"body": "# Deployment\n\nUpdated deployment instructions...",
"lastKnownHash": "sha256:abc123...",
"frontmatter": {
"title": "Deployment Guide (v2)"
}
}'
# Response: 200 OK with updated page
# If someone else edited the page since you read it:
# Response: 409 Conflict with current_hash — re-read and retry
When to use POST vs PUT
| Scenario | Method | What happens |
|---|---|---|
| Creating a brand new page | POST |
201 if created, 409 if path already taken |
| Updating an existing page | PUT |
200 if hash matches, 409 if page was modified by someone else |
| Page might or might not exist (scripts, imports) | POST, catch 409, then PUT |
Safe upsert pattern |
| AI agent writing via MCP | write_page tool |
Handles create/update automatically |
Authentication
Most endpoints require a JWT access token in the Authorization header:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Obtain tokens via the login or OIDC endpoints.
Token lifecycle
| Token | Lifetime | Purpose |
|---|---|---|
| Access token | 15 minutes | API authentication |
| Refresh token | 7 days | Obtain new access tokens (rotated on each use) |
API keys
For programmatic access (CI/CD, MCP, scripts), use API keys instead of JWT tokens. API keys use the dp_live_ prefix and are scoped to specific workspaces and permissions.
Authorization: Bearer dp_live_abc123...
Create API keys from Workspace Settings → API Keys.
Auth endpoints
Auth endpoints use the unversioned /api/auth/ prefix.
Register
POST /api/auth/register
Create a new user account. The first user becomes Super Admin.
Request:
{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "secure-password-here"
}
Response: 201 Created
{
"user": {
"id": "01HJK...",
"name": "Jane Smith",
"email": "jane@example.com",
"role": "superadmin"
},
"access_token": "eyJhbG...",
"refresh_token": "eyJhbG...",
"expires_in": 900
}
Login
POST /api/auth/login
Authenticate with email and password.
Request:
{
"email": "jane@example.com",
"password": "secure-password-here"
}
Response: 200 OK
{
"access_token": "eyJhbG...",
"refresh_token": "eyJhbG...",
"expires_in": 900
}
Errors:
| Code | Description |
|---|---|
401 |
Invalid credentials |
429 |
Too many login attempts (rate limited) |
Refresh token
POST /api/auth/refresh
Exchange a refresh token for a new access token. The refresh token is rotated (old one invalidated).
Request:
{
"refresh_token": "eyJhbG..."
}
Response: 200 OK
{
"access_token": "eyJhbG...",
"refresh_token": "eyJhbG...",
"expires_in": 900
}
Password reset request
POST /api/auth/forgot-password
Request a password reset token. With SMTP configured, an email is sent. Without SMTP, the token is logged to stdout.
Request:
{
"email": "jane@example.com"
}
Response: 200 OK (always, regardless of whether the email exists — prevents enumeration)
Password reset confirm
POST /api/auth/reset-password
Set a new password using a reset token.
Request:
{
"token": "reset-token-here",
"new_password": "new-secure-password"
}
Response: 200 OK
OIDC providers
GET /api/auth/providers — List available OIDC providers
POST /api/auth/oidc/:provider — Start OIDC flow (google or github)
GET /api/auth/oidc/:provider/callback — OIDC callback
POST /api/auth/oidc/claim — Claim OIDC tokens after callback
WebAuthn / Passkeys
POST /api/auth/webauthn/register/begin — Start passkey registration
POST /api/auth/webauthn/register/finish — Complete passkey registration
POST /api/auth/webauthn/login/begin — Start passkey login
POST /api/auth/webauthn/login/finish — Complete passkey login
GET /api/auth/webauthn/credentials — List stored credentials
DELETE /api/auth/webauthn/credentials/:id — Delete a credential
Other auth endpoints
POST /api/auth/logout — Logout (revoke refresh token)
GET /api/auth/me — Current user info
GET /api/auth/sessions — List active sessions
POST /api/auth/ws-token — Set WebSocket auth cookie
POST /api/auth/invitations/accept — Accept workspace invitation
Content endpoints
Content is addressed by workspace slug and file path, not by ID. All content endpoints use the /api/v1/content/{workspace}/{...path} pattern.
Get page
GET /api/v1/content/{workspace}/{...path}
Retrieve a page by its workspace slug and file path.
Example: GET /api/v1/content/my-docs/guides/getting-started
Response: 200 OK
{
"page_id": "01HJK...",
"path": "guides/getting-started.md",
"title": "Getting Started",
"description": "Install and configure DocPlatform.",
"content": "# Getting Started\n\nThis guide walks you through...",
"content_hash": "sha256:abc123...",
"frontmatter_hash": "sha256:def456...",
"tags": ["guide"],
"status": "published",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-16T14:30:00Z"
}
Errors:
| Code | Description |
|---|---|
403 |
Insufficient permissions |
404 |
Page not found or no read access |
Create page
POST /api/v1/content/{workspace}/{...path}
Create a new page at the given path. Returns 409 Conflict if a page already exists at this path — use PUT to update existing pages.
Request:
{
"title": "Getting Started",
"body": "# Getting Started\n\nWelcome to DocPlatform.",
"description": "Optional description",
"publish": false,
"tags": ["quickstart"]
}
Response: 201 Created with the page object.
Errors:
| Code | Description |
|---|---|
409 |
Page already exists at this path — use PUT to update |
400 |
Missing title or invalid path |
403 |
Insufficient permissions (requires write role) |
Update page
PUT /api/v1/content/{workspace}/{...path}
Update an existing page. Requires lastKnownHash for optimistic concurrency control — read the page first to get the current hash.
Request:
{
"body": "# Getting Started\n\nUpdated content...",
"lastKnownHash": "sha256:abc123...",
"frontmatter": {
"title": "Updated Title",
"tags": ["updated"]
}
}
The lastKnownHash field enables optimistic concurrency. Echo the content_hash from the server’s last response. If the hash doesn’t match the current version, the server returns 409 Conflict. For new pages, omit lastKnownHash.
Response: 200 OK — returns the full page object with updated hashes.
Errors:
| Code | Description |
|---|---|
409 |
Content hash mismatch — concurrent edit detected. Response includes current_hash, your_hash, modified_by, modified_at. |
Delete page
DELETE /api/v1/content/{workspace}/{...path}
Response: 204 No Content
Move/Rename page
PUT /api/v1/content/{workspace}/{...path}
Moving a page is a first-class operation. It preserves the stable page_id, updates all wikilinks across the workspace, and creates redirect aliases for the old URL.
Include the move_to field in the request body:
{
"move_to": "new/path/for/page"
}
Workspace endpoints
List workspaces
GET /api/v1/workspaces
Returns workspaces the current user is a member of.
Create workspace
POST /api/v1/workspaces
Requires Super Admin role.
Request:
{
"name": "API Docs",
"slug": "api-docs",
"git_remote": "git@github.com:org/api-docs.git",
"git_branch": "main"
}
Get workspace
GET /api/v1/workspaces/:id
Delete workspace
DELETE /api/v1/workspaces/:id
Soft-deletes the workspace.
Page tree
GET /api/v1/content/:workspace/tree
Returns the hierarchical page tree for a workspace, including nested structure and ordering.
Reorder pages
POST /api/v1/content/:workspace/reorder
Reorder pages within a tree level. Requires write permission.
Request:
{
"parent_path": "guides",
"order": [
{ "id": "01HJK...", "sort_order": 0 },
{ "id": "01HJL...", "sort_order": 1 },
{ "id": "01HJM...", "sort_order": 2 }
]
}
Response: 204 No Content
Search
GET /api/v1/workspaces/:id/search?q={query}
Full-text search across workspaces the user has access to.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
q |
string | Search query (required) |
workspace |
string | Filter by workspace slug |
tag |
string | Filter by tag |
limit |
int | Max results (default: 20) |
Response: 200 OK
{
"results": [
{
"page_id": "01HJK...",
"title": "Git Integration",
"path": "guides/git-integration.md",
"score": 0.95,
"snippet": "...bidirectional <mark>git sync</mark> lets your team..."
}
],
"total": 5,
"query": "git sync",
"took_ms": 12
}
Results are permission-filtered — users only see pages they have access to.
Git sync
Trigger sync
POST /api/v1/workspaces/{workspace_id}/sync
Manually trigger a git pull + reconciliation. Requires Admin role.
Response: 200 OK
{
"status": "completed",
"changes": {
"added": 2,
"updated": 1,
"deleted": 0
}
}
Webhook endpoint
POST /api/git/webhook/:workspace_id
A single endpoint that receives push event payloads from GitHub, GitLab, or Bitbucket. The payload format is auto-detected. No authentication header required — payloads are validated using the GIT_WEBHOOK_SECRET shared secret (HMAC-SHA256).
AI features
AI status
GET /api/v1/ai/status
Check whether AI features are enabled and which provider is configured.
Writing assist
POST /api/v1/ai/writing-assist
Rewrite, improve, shorten, or expand selected content.
Request:
{
"workspace_id": "01HJK...",
"operation": "improve",
"content": "This is the text to improve."
}
Operations: rewrite, improve, shorten, expand
Response: 200 OK
{
"result": "Here is the improved text..."
}
Doc chat
POST /api/v1/ai/chat
Multi-turn conversation about workspace documentation.
Request:
{
"workspace_id": "01HJK...",
"messages": [
{ "role": "user", "content": "How do I configure git sync?" }
]
}
Invitations
GET /api/v1/workspaces/:id/invitations — List pending invitations (admin)
POST /api/v1/workspaces/:id/invitations — Create invitation (admin)
DELETE /api/v1/workspaces/:id/invitations/:invitationId — Revoke invitation (admin)
Invitations are email-based with a 7-day TTL. Accepted via POST /api/auth/invitations/accept.
API keys
POST /api/v1/api-keys — Create API key (returns full key once)
GET /api/v1/api-keys — List API keys (prefix only)
DELETE /api/v1/api-keys/:id — Delete API key
POST /api/v1/api-keys/:id/rotate — Rotate API key
API keys use the dp_live_ prefix and are scoped to the organization.
Billing
Requires Stripe configuration. Disabled when STRIPE_SECRET_KEY is not set.
POST /api/v1/billing/checkout — Create Stripe Checkout session
POST /api/v1/billing/portal — Create Stripe Customer Portal session
GET /api/v1/billing/subscription — Current subscription status
GET /api/v1/billing/plans — Available plans and pricing
GET /api/v1/billing/limits — Plan limits and current usage
Stripe webhook
POST /api/webhooks/stripe
Receives Stripe webhook events (signature-verified). Handles subscription lifecycle events with idempotency.
Analytics
GDPR-compliant analytics with cookie consent. Feature-gated to paid plans.
POST /api/analytics/consent — Record GDPR consent
GET /api/v1/workspaces/:id/analytics/pages — Top pages (configurable days)
GET /api/v1/workspaces/:id/analytics/searches — Top search queries
GET /api/v1/workspaces/:id/analytics/overview — Dashboard overview
Doc versioning
Named documentation versions (e.g., v1, v2) within a workspace.
GET /api/v1/workspaces/:id/versions — List versions
POST /api/v1/workspaces/:id/versions — Create version (admin)
GET /api/v1/workspaces/:id/versions/:slug — Get version details
PUT /api/v1/workspaces/:id/versions/:slug/default — Set default version (admin)
DELETE /api/v1/workspaces/:id/versions/:slug — Delete version (admin)
Templates
GET /api/v1/templates — List available templates
GET /api/v1/templates/:id — Get template content
Quality scanner
GET /api/v1/workspaces/:id/quality
Scan a workspace for documentation quality issues. Returns readability scores, dead links, and completeness checks.
Static export
GET /api/v1/workspaces/:id/export
Export all published pages as a static HTML ZIP file.
Custom domains
PUT /api/v1/workspaces/:id/custom-domain — Set custom domain (admin)
GET /api/v1/workspaces/:id/custom-domain — Get domain status (admin)
DELETE /api/v1/workspaces/:id/custom-domain — Remove domain (admin)
Workspace admin
GET /api/v1/workspaces/:id/admin/members — List members
PUT /api/v1/workspaces/:id/admin/members/:user_id/role — Update member role
GET /api/v1/workspaces/:id/admin/settings — Get workspace settings
PUT /api/v1/workspaces/:id/admin/settings — Update workspace settings
Onboarding
GET /api/v1/users/me/onboarding — Get onboarding state
PATCH /api/v1/users/me/onboarding — Update onboarding state
Super admin panel
These endpoints require the Super Admin role. All prefixed with /api/v1/admin/.
Organization management
GET /api/v1/admin/orgs — List all organizations
GET /api/v1/admin/orgs/:id — Get organization details
PUT /api/v1/admin/orgs/:id/plan — Change organization plan
POST /api/v1/admin/orgs/:id/subscription/override — Override subscription
PUT /api/v1/admin/orgs/:id/rate-limits — Override rate limits
POST /api/v1/admin/orgs/:id/export — Export org data (GDPR)
User management
GET /api/v1/admin/users — List all users
GET /api/v1/admin/users/:id — Get user details
POST /api/v1/admin/users/:id/impersonate — Impersonate user
POST /api/v1/admin/users/:id/export — Export user data (GDPR)
DELETE /api/v1/admin/users/:id — Delete user (GDPR right to erasure)
Audit log
GET /api/v1/admin/audit-log — Query audit log (filterable by user, action, resource, date range)
Billing overview
GET /api/v1/admin/billing/overview — Platform-wide billing summary
GET /api/v1/admin/billing/subscriptions — All active subscriptions
GET /api/v1/admin/billing/events — Webhook event log
Domain management
GET /api/v1/admin/domains — List all custom domains
POST /api/v1/admin/domains/:id/verify — Verify domain DNS
POST /api/v1/admin/domains/:id/provision — Provision TLS certificate
DELETE /api/v1/admin/domains/:id — Delete domain
System health
GET /api/v1/admin/system/health — System health metrics (disk, memory, uptime, DB stats)
Platform analytics
GET /api/v1/admin/analytics/overview — Platform-wide analytics overview
GET /api/v1/admin/analytics/growth — Platform growth metrics
MCP server
DocPlatform includes a built-in MCP (Model Context Protocol) server exposing 24 tools for AI agent integration. Two transports are available:
# stdio (single workspace, for local AI tools)
docplatform mcp --workspace my-docs --api-key dp_live_abc123
# Streamable HTTP (multi-workspace, for remote/cloud access)
docplatform mcp-server --addr :8081
MCP tools (24)
| Tool | Category | Description |
|---|---|---|
docplatform_list_pages |
Content | List all pages with paths and titles |
docplatform_read_page |
Content | Read full page content, frontmatter, and metadata |
docplatform_write_page |
Content | Smart upsert — creates or updates automatically |
docplatform_update_page |
Content | Update with optimistic concurrency (requires last_known_hash) |
docplatform_delete_page |
Content | Soft-delete a page |
docplatform_move_page |
Content | Move/rename with automatic wikilink updates |
docplatform_search |
Discovery | Full-text search with scored results |
docplatform_get_context |
Discovery | RAG bundle: page + parent + siblings + linked pages |
docplatform_list_workspaces |
Discovery | List accessible workspaces |
docplatform_get_tree |
Discovery | Hierarchical page tree |
docplatform_get_manifest |
Discovery | Complete workspace manifest with link relationships |
docplatform_validate_links |
Quality | Scan for broken wikilinks |
docplatform_quality_scan |
Quality | Quality score (0–100) with detailed findings |
docplatform_get_theme |
Settings | Get current published site theme |
docplatform_update_theme |
Settings | Update theme (default, dark, forest, rose, amber, minimal, corporate) |
docplatform_create_workspace |
Management | Create a new workspace |
docplatform_get_workspace |
Management | Workspace details, role, git sync, publish settings |
docplatform_list_versions |
Versioning | List documentation versions |
docplatform_create_version |
Versioning | Create a named version (e.g., v2.0) |
docplatform_export |
Export | Export workspace as static site |
docplatform_writing_assist |
AI | Improve, shorten, expand, fix grammar, summarize, translate |
docplatform_get_activity |
Activity | Recent workspace activity feed |
docplatform_list_comments |
Comments | Threaded comments on a page |
docplatform_add_comment |
Comments | Add a comment with @mentions and threading |
All tools respect workspace permissions and require a valid API key. See the MCP Server guide for complete setup and tool reference.
Prometheus metrics
GET /metrics
Available when FF_METRICS=true. Requires Super Admin authentication. Exposes HTTP latency histograms, request counts, auth event counters, and more.
Health
These endpoints use the unversioned /api/ prefix and do not require authentication.
GET /api/health → 200 OK { "status": "ok", "db": "ok", "git": "ok" }
GET /api/ready → 200 OK { "status": "ready" }
The /api/ready endpoint returns 503 Service Unavailable while the initial reconciliation is in progress (startup).
Error format
All error responses follow RFC 7807 (Problem Details for HTTP APIs):
{
"type": "https://docplatform.io/errors/conflict-detected",
"title": "Conflict Detected",
"status": 409,
"detail": "Content hash mismatch. The page was modified by another user.",
"current_hash": "sha256:def456...",
"your_hash": "sha256:abc123...",
"modified_by": "jane@example.com",
"modified_at": "2025-01-16T14:30:00Z"
}
Validation errors include a fields array:
{
"type": "https://docplatform.io/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "One or more fields are invalid.",
"fields": [
{ "field": "content", "error": "required" },
{ "field": "lastKnownHash", "error": "must be 64 hex characters" }
]
}
Common error codes
| HTTP | Code | Description |
|---|---|---|
400 |
VALIDATION_ERROR |
Invalid request body or parameters |
401 |
UNAUTHORIZED |
Missing or invalid authentication |
403 |
FORBIDDEN |
Insufficient permissions |
404 |
NOT_FOUND |
Resource not found (or no read access) |
409 |
CONFLICT_DETECTED |
Concurrent modification detected |
422 |
UNPROCESSABLE |
Valid syntax but semantic error (e.g., circular wikilink) |
429 |
RATE_LIMITED |
Too many requests |
500 |
INTERNAL_ERROR |
Server error (check logs) |
503 |
SERVICE_UNAVAILABLE |
Reconciliation in progress |
Pagination
Content listing endpoints use cursor-based pagination with ULIDs for stable results even when content is being added or deleted.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor |
string | — | ULID of the last item from the previous page. Omit for the first page. |
limit |
int | 20 | Number of results per page (max: 100) |
Response metadata:
{
"data": [...],
"next_cursor": "01HJK...",
"has_more": true
}
Pass next_cursor as the cursor parameter in the next request. When has_more is false, you’ve reached the end.
Asset uploads
POST /api/v1/content/{workspace}/assets
Upload images and files to a workspace. Assets are stored in the workspace’s assets/ directory and committed to git if sync is enabled.
Request: multipart/form-data with a file field.
Limits:
| Constraint | Value |
|---|---|
| Max file size | 10 MB |
| Accepted types | PNG, JPG, GIF, SVG, WebP, PDF |
Response: 201 Created
{
"path": "assets/screenshot-2025-01-15.png",
"url": "/api/v1/content/{workspace}/assets/screenshot-2025-01-15.png",
"size": 245760,
"content_type": "image/png"
}
Conflict resolution
List conflicts
GET /api/v1/workspaces/{workspace_id}/conflicts
Response: 200 OK
{
"workspace_id": "01HJK...",
"sync_status": "conflict",
"conflicts": [
{
"path": "guides/editor.md",
"ours_hash": "abc123...",
"theirs_hash": "def456...",
"page_id": "01HJK...",
"timestamp": "20250115T103045Z"
}
]
}
Download a conflict version
GET /api/v1/conflicts/{page_id}/{timestamp}/{version}
The version parameter is either ours (local) or theirs (remote).
Response: 200 OK with Content-Type: text/markdown — raw file content.
Resolve a conflict
POST /api/v1/conflicts/{page_id}/{timestamp}/resolve
Removes conflict artifacts after manual resolution.
Response: 200 OK
{
"message": "Conflict resolved",
"page_id": "01HJK..."
}
WebSocket
Obtain a WebSocket auth cookie
POST /api/auth/ws-token
WebSocket connections use an HttpOnly cookie mechanism to avoid exposing tokens in URLs.
Response: 200 OK
Sets a dp_ws_token HttpOnly cookie (valid for 30 seconds, single-use). No JSON body is returned.
Connect via:
ws://localhost:3000/ws
The browser sends the dp_ws_token cookie automatically. The server validates it, establishes the WebSocket, and clears the cookie.
Server events
| Event type | Payload | When |
|---|---|---|
page-created |
{workspace_id, path, actor} |
A new page is created |
page-updated |
{workspace_id, path, actor} |
A page is modified |
page-deleted |
{workspace_id, path, actor} |
A page is deleted |
presence-join |
{workspace_id, user_id} |
A user connects |
presence-leave |
{workspace_id, user_id} |
A user disconnects (90s timeout) |
sync-status |
{workspace_id, status} |
Git sync status change |
conflict-detected |
{workspace_id, path} |
Git merge conflict found |
page-moved |
{workspace_id, old_path, new_path, actor} |
A page is moved/renamed |
bulk-sync |
{workspace_id, changed_count, paths[]} |
Multiple files synced in one pull |
Client messages
{"type": "subscribe", "workspace_id": "01HJK..."}
{"type": "unsubscribe", "workspace_id": "01HJK..."}
Security headers
DocPlatform sets the following headers on all responses:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Content-Security-Policy |
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' |
X-Request-ID |
ULID (unique per request, included in error responses and logs) |
Published docs additionally set:
| Header | Value |
|---|---|
Cache-Control |
public, max-age=300 |
ETag |
Content hash of the rendered page |
Rate limiting
Rate limits are tier-based and scale with your plan. Token bucket algorithm, per-org for authenticated requests, per-IP for unauthenticated.
| Endpoint category | Community / Free | Team | Business | Enterprise |
|---|---|---|---|---|
| Content read | 100/min | 300/min | 600/min | 1,200/min |
| Content write | 20/min | 60/min | 120/min | 300/min |
| Search | 30/min | 100/min | 200/min | 500/min |
| Auth (login, register, reset) | 5/min per IP | 5/min | 5/min | 5/min |
| Git webhooks | 10/min | 30/min | 60/min | 120/min |
| Published docs (public) | 1,000/min per IP | 3,000/min | 6,000/min | Unlimited |
Rate limit responses include these headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1234567890
Retry-After: 30