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 an ETag; send it back in If-None-Match to get 304 Not Modified when 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 path is <workspace>/<folders…>/<doc-slug>, i.e. what follows /org/<orgSlug>/ in its URL.

Authentication

http
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:

PlanSustainedBurstActive keys
Free30 req/min301
Team120 req/min1205
Pro600 req/min30020

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

StatusBodyMeaning
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
403 plan_featurejson
{
  "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:

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

GETdocsAll plans

Published, non-archived documents the key can see, newest-updated first. Keyset-paginated: pass next_cursor back as cursor until it is null.

Query parameters

NameTypeDescription
folderstring<workspace> or <workspace>/<folders…>; that subtree only (subfolders included).
updated_sinceISO 8601Only documents updated at or after this time (incremental sync).
qstringFull-text filter. Team+; on Free it answers 403 plan_feature.
cursorstringOpaque cursor from the previous page's next_cursor.
limitinteger1–200, default 200.
bash
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
  "https://versodocs.com/api/v1/orgs/acme/docs?folder=eng/apis&limit=50"
200json
{
  "documents": [ { "id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e", "title": "Payments API", "path": "eng/apis/payments-api", … } ],
  "next_cursor": "eyJ1IjoxNzI3Nzc…"
}
GETdocs/{documentId}All plans · version: Team+

One document and one published version: Current by default.

Query parameters

NameTypeDescription
formatjson | md | htmlContent format: Tiptap JSON (default), Markdown, or HTML.
versionintegerA past published version number. Team+.
bash
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
  "https://versodocs.com/api/v1/orgs/acme/docs/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e?format=md"
200json
{
  …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.

GETdocs/by-pathAll plans · version: Team+

Same as docs/{documentId} (without the legacy keys), addressed by path.

Query parameters

NameTypeDescription
path *string<workspace>/<folders…>/<doc-slug>
formatjson | md | htmlDefault json.
versionintegerPast version. Team+.
bash
curl -H "Authorization: Bearer $VERSODOCS_TOKEN" \
  "https://versodocs.com/api/v1/orgs/acme/docs/by-path?path=eng/apis/payments-api&format=md"
GETdocs/{documentId}/versionsTeam+

Every published version, newest first (up to 500). Read one with docs/{documentId}?version=N.

200json
{
  "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" }
    }
  ]
}
GETdocs/{documentId}/diffTeam+

Unified diff of two published versions' Markdown, plus block and line counts.

Query parameters

NameTypeDescription
from *integerOlder version number.
tointegerNewer version number. Default: Current.
bash
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
200json
{
  "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@@ …"
}
GETtreeTeam+

Workspace → folder → document hierarchy. Only published, non-archived documents appear.

Query parameters

NameTypeDescription
workspacestringLimit to one workspace slug.
200json
{
  "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": []
  }]
}
GETdocs/{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

NameTypeDescription
versionintegerPublished version number.
200json
{
  "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"
  }]
}
GETdocs/{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.

bash
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"
GETdocs/{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.

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

GETdocs/currentTeam+

Same stamp, addressed by path, for references written as document URLs.

Query parameters

NameTypeDescription
path *string<workspace>/<folders…>/<doc-slug>
POSTdocs/{documentId}/proposalsPro · docs:propose

Opens 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

NameTypeDescription
title *string1–200 characters.
descriptionstringContext for reviewers (up to 10,000 characters).
content_markdownstringThe complete new document, up to 500,000 characters. Send this or edits. ![alt](https://…) 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_reviewstring[]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.

Partial editbash
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"
bash
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"
201json
{
  "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).

POSTdocs/{documentId}/attachmentsPro · docs:propose or docs:write

Upload 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? }.

bash
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"
201json
{
  "id": "5d1e9f0a2b3c4d5e6f708192a3b4c5d6",
  "kind": "image",
  "name": "checkout-flow.png", "mime": "image/png", "size": 48213,
  "url": "https://pub-….r2.dev/doc/…/img/5d1e9f0a2b3c4d5e6f708192a3b4c5d6.png",
  "markdown": "![Checkout flow](https://pub-….r2.dev/doc/…/img/5d1e9f0a2b3c4d5e6f708192a3b4c5d6.png)"
}

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.

bash
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.

MethodPathWhatMinimum planScope
GETdocsList published documentsFreedocs:read
GETdocs/{documentId}Read one documentFreedocs:read
GETdocs/by-pathRead a document by its pathFreedocs:read
GETdocs/{documentId}/versionsList published versionsTeamdocs:read
GETdocs/{documentId}/diffDiff two published versionsTeamdocs:read
GETsearchFull-text searchTeamdocs:read
GETtreeWorkspace → folder → document treeTeamdocs:read
GETdocs/{documentId}/attachmentsList attachments of a versionTeamdocs:read
GETdocs/{documentId}/attachments/{attachmentId}Download an attachment (302)Teamdocs:read
GETdocs/{documentId}/currentCurrent version stamp (CI check)Teamdocs:read
GETdocs/currentCurrent version stamp by path (CI check)Teamdocs:read
POSTdocs/{documentId}/proposalsPropose a change for reviewProdocs:propose
POSTdocs/{documentId}/attachmentsUpload an image or file to embed in a proposalProdocs:propose
POSTdocs/{documentId}/attachments/{attachmentId}/confirmConfirm a presigned uploadProdocs:propose
GETproposalsList proposalsTeamproposals:review
GETproposals/{proposalId}Read a proposal (diff, reviewers, comments)Teamproposals:review
POSTproposals/{proposalId}/commentsComment or reply on a proposalTeamproposals:review
PATCHproposals/{proposalId}/comments/{commentId}Resolve or reopen a comment threadTeamproposals:review
GETdocs/{documentId}/commentsComment threads on a published versionTeamdocs:read
POSTdocs/{documentId}/commentsComment or reply on a documentTeamproposals:review
PATCHdocs/{documentId}/comments/{commentId}Resolve or reopen a document comment threadTeamproposals:review
POSTproposals/{proposalId}/reviewsSubmit an advisory verdictTeamproposals:review
PATCHproposals/{proposalId}Revise the agent’s own open proposalProdocs:propose
POSTproposals/{proposalId}/closeWithdraw the agent’s own proposalProdocs:propose
POSTproposals/{proposalId}/publishPublish an approved proposalProproposals:publish
POSTdocsCreate a documentProdocs:write
PATCHdocs/{documentId}Rename or move a documentProdocs:write
DELETEdocs/{documentId}Delete an unpublished documentProdocs:write
POSTfoldersCreate a folderProdocs:write
PATCHfolders/{folderId}Rename or move a folderProdocs:write
DELETEfolders/{folderId}Delete an empty folderProdocs:write
GETdocs/{documentId}/issue-linksIssues linked to a documentTeamdocs:read
POSTdocs/{documentId}/issue-linksLink an issue to a documentProdocs:write
GETproposals/{proposalId}/issue-linksIssues linked to a proposalTeamproposals:review
POSTproposals/{proposalId}/issue-linksLink an issue to a proposalTeamproposals:review
GETcontext-packAll Current docs in one Markdown / JSON packTeamdocs:read
GETllms.txtPrivate llms.txt index of Current docsTeamdocs:read
GETwebhooksList automation subscriptionsFreedocs:read
POSTwebhooksSubscribe a URL to events (Zapier, n8n)Freedocs:read
DELETEwebhooks/{webhookId}UnsubscribeFreedocs:read
GETwebhooks/samplesSample deliveries for an eventFreedocs:read