Developer docs
REST API reference
Read your organization's published documents over HTTPS, and on Pro let automation propose changes that go through normal review (on Team, agents propose through a one-click MCP connection). Reviewing proposals, publishing approved ones and managing docs & folders are covered in Agent workflows. Machine-readable: openapi.json (OpenAPI 3.1).
Basics
- Base URL:
https://versodocs.com/api/v1/orgs/<orgSlug>/. The org slug is the segment after/org/in your VersoDocs URLs. - Responses are JSON with
Cache-Control: private. Document, version, diff and attachment-list responses carry anETag; send it back inIf-None-Matchto get304 Not Modifiedwhen nothing changed. Published versions are immutable, so polling is cheap. - Document reads return only published versions. Archived, unpublished and out-of-scope documents answer
404, exactly like unknown ones. Proposal text is only readable through the review endpoints (proposals:review); drafts never leave VersoDocs. - A document's
pathis<workspace>/<folders…>/<doc-slug>, i.e. what follows/org/<orgSlug>/in its URL.
Authentication
Authorization: Bearer vsd_…Create keys in Organization settings → Developer (owners and admins). Keys never expire; revoke them at any time. Every key holds docs:read; on Team it can add proposals:review, and on Pro also docs:propose, docs:write and proposals:publish (the last two are opt-in). On Team, only one-click MCP connections may propose, capped monthly. A Pro key can also be bound to a single workspace. The full scope table is in Agent workflows.
OAuth access tokens (vsd_at_…, issued when someone connects an AI app to the MCP server) are audience-bound to /api/mcp: the REST API answers 401 to them. Use an API key here.
On Free, keys see only the org's active workspace (the Free single-workspace rule). If the org has several workspaces and none has been chosen yet, every call answers 409 choose_active_workspace until an owner picks one in the app or upgrades.
Rate limits
A token bucket per key (or per connected app), sized by the org's plan and shared between the REST API and the MCP server:
| Plan | Sustained | Burst | Active keys |
|---|---|---|---|
| Free | 30 req/min | 30 | 1 |
| Team | 120 req/min | 120 | 5 |
| Pro | 600 req/min | 300 | 20 |
Over the limit you get 429 with a Retry-After header (seconds) and limit_per_min in the body.
Errors
Errors are JSON. error is a machine code (or, for plan errors, a human sentence with the code in code).
| Status | Body | Meaning |
|---|---|---|
| 400 | { error: "bad_request", message } | Invalid parameter or body |
| 401 | { error: "unauthorized", message } | Missing, unknown, revoked or expired token; OAuth token used on REST; authorizing member left the org |
| 403 | { error, code: "plan_feature", feature, required_plan, upgrade_url } | Endpoint or parameter not included in the org’s plan |
| 403 | { error: "forbidden", message } | Key lacks the scope, or its creator can’t edit the document |
| 404 | { error: "not_found", message? } | Unknown, archived, unpublished, outside the key’s workspaces, or another org |
| 409 | { error, code: "choose_active_workspace", upgrade_url } | Free org with several workspaces and no active one chosen |
| 409 | { error: "proposal_limit", message } | Open-proposal cap reached for the document |
| 409 | { error: "key_owner_missing", message } | The key’s creator no longer exists (writes only) |
| 422 | { error: "edit_failed", message } | An edits[i].find is missing or ambiguous; nothing was written |
| 429 | { error: "rate_limited", message, limit_per_min } | Slow down; honour Retry-After |
{
"error": "Version history via the API requires the Team plan.",
"code": "plan_feature",
"feature": "history",
"required_plan": "team",
"upgrade_url": "https://versodocs.com/org/acme/billing"
}Review, publish and docs & folders endpoints add their own codes (requires_e_signature, not_approved, stale, published_document, folder_not_empty, …): see Agent workflows.
Document envelope
Every document in a response shares these fields:
{
"id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"title": "Payments API",
"slug": "payments-api",
"workspace_slug": "eng",
"path": "eng/apis/payments-api",
"url": "https://versodocs.com/org/acme/eng/apis/payments-api",
"updated_at": "2026-10-01T09:12:44.120Z",
"current_version": {
"number": 13,
"label": "GA",
"published_at": "2026-09-30T16:00:00.000Z"
},
"reference": "versodocs:acme/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e@v13"
}reference is the canonical, citable spec reference (versodocs:<org>/<id>@v<N>). Paste it in pull requests; the spec drift check flags it once a newer version is Current. current_version and reference are null only for a document that has never been published (never returned by list endpoints).
Endpoints
Paths below are relative to the base URL. Every endpoint needs the docs:read scope unless noted.
docsAll plansPublished, non-archived documents the key can see, newest-updated first. Keyset-paginated: pass next_cursor back as cursor until it is null.
Query parameters
| Name | Type | Description |
|---|---|---|
folder | string | <workspace> or <workspace>/<folders…>; that subtree only (subfolders included). |
updated_since | ISO 8601 | Only documents updated at or after this time (incremental sync). |
q | string | Full-text filter. Team+; on Free it answers 403 plan_feature. |
cursor | string | Opaque cursor from the previous page's next_cursor. |
limit | integer | 1–200, default 200. |
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
"https://versodocs.com/api/v1/orgs/acme/docs?folder=eng/apis&limit=50"{
"documents": [ { "id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e", "title": "Payments API", "path": "eng/apis/payments-api", … } ],
"next_cursor": "eyJ1IjoxNzI3Nzc…"
}docs/{documentId}All plans · version: Team+One document and one published version: Current by default.
Query parameters
| Name | Type | Description |
|---|---|---|
format | json | md | html | Content format: Tiptap JSON (default), Markdown, or HTML. |
version | integer | A past published version number. Team+. |
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e?format=md"{
…envelope,
"version": {
"id": "5d1e…",
"slug": "…",
"number": 13,
"label": "GA",
"published_at": "2026-09-30T16:00:00.000Z",
"is_current": true,
"reference": "versodocs:acme/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e@v13",
"url": "https://versodocs.com/org/acme/eng/apis/payments-api?v=13",
"author": { "name": "Dana Ops", "email": "dana@acme.com" },
"version_number": 13,
"created_at": "2026-09-30T16:00:00.000Z",
"format": "md",
"content_markdown": "# Payments API\n…"
},
"format": "md",
"content": "# Payments API\n…",
"document": { "id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e", "title": "Payments API", "slug": "payments-api", "workspace_slug": "eng", "org_slug": "acme" }
}document and the version.version_number / created_at / content* keys are kept for backward compatibility; prefer the top-level fields.
docs/by-pathAll plans · version: Team+Same as docs/{documentId} (without the legacy keys), addressed by path.
Query parameters
| Name | Type | Description |
|---|---|---|
path * | string | <workspace>/<folders…>/<doc-slug> |
format | json | md | html | Default json. |
version | integer | Past version. Team+. |
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
"https://versodocs.com/api/v1/orgs/acme/docs/by-path?path=eng/apis/payments-api&format=md"docs/{documentId}/versionsTeam+Every published version, newest first (up to 500). Read one with docs/{documentId}?version=N.
{
"document": { …envelope },
"versions": [
{
"id": "5d1e…", "number": 13, "label": "GA",
"published_at": "2026-09-30T16:00:00.000Z", "is_current": true,
"reference": "versodocs:acme/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e@v13",
"url": "https://versodocs.com/org/acme/eng/apis/payments-api?v=13",
"author": { "name": "Dana Ops", "email": "dana@acme.com" }
}
]
}docs/{documentId}/diffTeam+Unified diff of two published versions' Markdown, plus block and line counts.
Query parameters
| Name | Type | Description |
|---|---|---|
from * | integer | Older version number. |
to | integer | Newer version number. Default: Current. |
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/diff?from=12" | jq -r .diff{
"document": { …envelope },
"from": { "number": 12, "label": null, "published_at": "…" },
"to": { "number": 13, "label": "GA", "published_at": "…" },
"summary": {
"blocks_added": 2, "blocks_removed": 0, "blocks_changed": 1, "blocks_unchanged": 41,
"lines_added": 9, "lines_removed": 3
},
"format": "unified",
"diff": "--- eng/apis/payments-api@v12\n+++ eng/apis/payments-api@v13\n@@ …"
}searchTeam+Ranked full-text search over published documents; the last word is prefix-matched.
Query parameters
| Name | Type | Description |
|---|---|---|
q * | string | 2–200 characters. |
limit | integer | 1–50, default 20. |
{ "query": "retry policy", "results": [ { …envelope, "rank": 0.42 } ] }treeTeam+Workspace → folder → document hierarchy. Only published, non-archived documents appear.
Query parameters
| Name | Type | Description |
|---|---|---|
workspace | string | Limit to one workspace slug. |
{
"workspaces": [{
"id": "…", "name": "Engineering", "slug": "eng",
"folders": [{
"id": "…", "name": "APIs", "slug": "apis", "path": "eng/apis",
"folders": [],
"documents": [{ "id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e", "title": "Payments API", "slug": "payments-api",
"path": "eng/apis/payments-api", "current_version_number": 13 }]
}],
"documents": []
}]
}docs/{documentId}/attachmentsTeam+Files and uploaded images embedded in a published version (Current unless version is given). type is image or file; size is 0 when unknown (images).
Query parameters
| Name | Type | Description |
|---|---|---|
version | integer | Published version number. |
{
"document_id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"version": 13,
"attachments": [{
"id": "9f8e7d6c5b4a39281706f5e4d3c2b1a0", "type": "file",
"name": "sequence.pdf", "mime": "application/pdf", "size": 48213,
"download_url": "https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/attachments/9f8e7d6c5b4a39281706f5e4d3c2b1a0"
}]
}docs/{documentId}/attachments/{attachmentId}Team+302 redirect to a signed download URL valid for 5 minutes. Only files referenced by that published version are reachable. Optional version.
curl -L -o sequence.pdf -H "Authorization: Bearer $VERSODOCS_TOKEN" \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/attachments/9f8e7d6c5b4a39281706f5e4d3c2b1a0"docs/{documentId}/currentTeam+The Current version stamp without content: what CI needs to tell whether a pinned spec moved on. Used by the GitHub spec drift action; the shape is frozen.
{
"document": {
"id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e", "title": "Payments API", "slug": "payments-api",
"org_slug": "acme", "workspace_slug": "eng",
"url": "https://versodocs.com/org/acme/eng/apis/payments-api",
"history_url": "https://versodocs.com/org/acme/eng/apis/payments-api?view=history"
},
"current_version": {
"number": 13, "label": "GA", "published_at": "2026-09-30T16:00:00.000Z",
"url": "https://versodocs.com/org/acme/eng/apis/payments-api?v=13",
"change_url": "https://versodocs.com/org/acme/eng/proposals/…"
},
"reference": "versodocs:acme/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e@v13"
}change_url is the proposal that produced Current, or null after a direct publish or restore.
docs/currentTeam+Same stamp, addressed by path, for references written as document URLs.
Query parameters
| Name | Type | Description |
|---|---|---|
path * | string | <workspace>/<folders…>/<doc-slug> |
docs/{documentId}/proposalsPro · docs:proposeOpens a proposal in review, authored by the key's creator, who must be able to edit the document. It follows the normal review → publish workflow; this call never publishes. Reviewers are matched by email among org members. The same creation path serves MCP propose_change, where Team organizations get 25 agent proposals a month (403, code: "agent_proposal_quota" beyond that).
JSON body
| Name | Type | Description |
|---|---|---|
title * | string | 1–200 characters. |
description | string | Context for reviewers (up to 10,000 characters). |
content_markdown | string | The complete new document, up to 500,000 characters. Send this or edits.  embeds an image; to add a new image or file, upload it and paste the markdown it returns. |
edits | { find, replace }[] | Partial edit: 1–100 replacements applied in order to the Current Markdown. Each find is literal, case-sensitive and must match exactly once; otherwise 422 edit_failed names the failing edit and nothing is written. An empty replace deletes. |
request_review | string[] | Up to 20 reviewer emails. |
Prefer edits for small changes to long documents: read the Current Markdown first (?format=md) and copy the text to replace exactly, including punctuation and Markdown markers.
curl -X POST -H "Authorization: Bearer $VERSODOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Retries: 5 attempts",
"edits": [
{ "find": "Retries up to 3 times", "replace": "Retries up to 5 times" },
{ "find": "## Deprecated endpoints\n", "replace": "" }
]
}' \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/proposals"curl -X POST -H "Authorization: Bearer $VERSODOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Clarify retry semantics",
"description": "Generated by the release agent from PR #482.",
"content_markdown": "# Payments API\n\n…the full new document…",
"request_review": ["lead@acme.com"]
}' \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/proposals"{
"proposal_id": "c3a1…",
"url": "https://versodocs.com/org/acme/eng/proposals/c3a1…",
"status": "in_review",
"document_id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"reviewers_requested": ["lead@acme.com"],
"unmatched_reviewers": []
}Every API-created proposal is written to the audit log (api.proposal_created, with the key name). On Pro, API usage is also sampled into the audit log (api.request, at most one row per key × endpoint × hour).
docs/{documentId}/attachmentsPro · docs:propose or docs:writeUpload an image or file to embed in a proposal on this document. The answer carries a markdown snippet: put it in content_markdown (or an edit's replace) and the proposal shows the image inline, or the file as an attachment. Images: PNG, JPEG, GIF, WebP up to 10.0 MB (no SVG; the bytes must really be that type). Files: PDF, Word, Excel, PowerPoint, CSV, text, Markdown, JSON, ZIP up to 25.0 MB. The key's creator must be able to edit the document, and uploads count toward the plan's storage. Uploads no proposal uses are removed after a day.
Direct (up to 5.0 MB): multipart form data with a file field (optional alt), or JSON { filename, content_type, data_base64, alt? }.
curl -X POST -H "Authorization: Bearer $VERSODOCS_TOKEN" \
-F "file=@checkout-flow.png" -F "alt=Checkout flow" \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/attachments"{
"id": "5d1e9f0a2b3c4d5e6f708192a3b4c5d6",
"kind": "image",
"name": "checkout-flow.png", "mime": "image/png", "size": 48213,
"url": "https://pub-….r2.dev/doc/…/img/5d1e9f0a2b3c4d5e6f708192a3b4c5d6.png",
"markdown": ""
}Presigned (larger files; bytes go straight to storage): send JSON { filename, content_type, size }, PUT the file to upload.url with exactly upload.headers within 10 minutes, then POST confirm_url.
curl -X POST -H "Authorization: Bearer $VERSODOCS_TOKEN" -H "Content-Type: application/json" \
-d '{ "filename": "load-test.pdf", "content_type": "application/pdf", "size": 9437184 }' \
"https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/attachments"
# → { "attachment": { "id": "…", "markdown": "…" },
# "upload": { "url": "https://…", "method": "PUT", "headers": { … }, "expires_in": 600 },
# "confirm_url": "https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/attachments/<id>/confirm" }
curl -X PUT -H "Content-Type: application/pdf" \
-H 'Content-Disposition: inline; filename="load-test.pdf"' \
--data-binary @load-test.pdf "$UPLOAD_URL"
curl -X POST -H "Authorization: Bearer $VERSODOCS_TOKEN" "$CONFIRM_URL"Errors: 413 file_too_large, 415 unsupported_type or content_mismatch, 403 storage_quota when the plan's storage is full, 404 upload_not_found from confirm when nothing was PUT.
All endpoints
Review, document comments, revise, publish and docs & folders endpoints are documented with examples in Agent workflows.
| Method | Path | What | Minimum plan | Scope |
|---|---|---|---|---|
GET | docs | List published documents | Free | docs:read |
GET | docs/{documentId} | Read one document | Free | docs:read |
GET | docs/by-path | Read a document by its path | Free | docs:read |
GET | docs/{documentId}/versions | List published versions | Team | docs:read |
GET | docs/{documentId}/diff | Diff two published versions | Team | docs:read |
GET | search | Full-text search | Team | docs:read |
GET | tree | Workspace → folder → document tree | Team | docs:read |
GET | docs/{documentId}/attachments | List attachments of a version | Team | docs:read |
GET | docs/{documentId}/attachments/{attachmentId} | Download an attachment (302) | Team | docs:read |
GET | docs/{documentId}/current | Current version stamp (CI check) | Team | docs:read |
GET | docs/current | Current version stamp by path (CI check) | Team | docs:read |
POST | docs/{documentId}/proposals | Propose a change for review | Pro | docs:propose |
POST | docs/{documentId}/attachments | Upload an image or file to embed in a proposal | Pro | docs:propose |
POST | docs/{documentId}/attachments/{attachmentId}/confirm | Confirm a presigned upload | Pro | docs:propose |
GET | proposals | List proposals | Team | proposals:review |
GET | proposals/{proposalId} | Read a proposal (diff, reviewers, comments) | Team | proposals:review |
POST | proposals/{proposalId}/comments | Comment or reply on a proposal | Team | proposals:review |
PATCH | proposals/{proposalId}/comments/{commentId} | Resolve or reopen a comment thread | Team | proposals:review |
GET | docs/{documentId}/comments | Comment threads on a published version | Team | docs:read |
POST | docs/{documentId}/comments | Comment or reply on a document | Team | proposals:review |
PATCH | docs/{documentId}/comments/{commentId} | Resolve or reopen a document comment thread | Team | proposals:review |
POST | proposals/{proposalId}/reviews | Submit an advisory verdict | Team | proposals:review |
PATCH | proposals/{proposalId} | Revise the agent’s own open proposal | Pro | docs:propose |
POST | proposals/{proposalId}/close | Withdraw the agent’s own proposal | Pro | docs:propose |
POST | proposals/{proposalId}/publish | Publish an approved proposal | Pro | proposals:publish |
POST | docs | Create a document | Pro | docs:write |
PATCH | docs/{documentId} | Rename or move a document | Pro | docs:write |
DELETE | docs/{documentId} | Delete an unpublished document | Pro | docs:write |
POST | folders | Create a folder | Pro | docs:write |
PATCH | folders/{folderId} | Rename or move a folder | Pro | docs:write |
DELETE | folders/{folderId} | Delete an empty folder | Pro | docs:write |
GET | docs/{documentId}/issue-links | Issues linked to a document | Team | docs:read |
POST | docs/{documentId}/issue-links | Link an issue to a document | Pro | docs:write |
GET | proposals/{proposalId}/issue-links | Issues linked to a proposal | Team | proposals:review |
POST | proposals/{proposalId}/issue-links | Link an issue to a proposal | Team | proposals:review |
GET | context-pack | All Current docs in one Markdown / JSON pack | Team | docs:read |
GET | llms.txt | Private llms.txt index of Current docs | Team | docs:read |
GET | webhooks | List automation subscriptions | Free | docs:read |
POST | webhooks | Subscribe a URL to events (Zapier, n8n) | Free | docs:read |
DELETE | webhooks/{webhookId} | Unsubscribe | Free | docs:read |
GET | webhooks/samples | Sample deliveries for an event | Free | docs:read |