A Document is a named series of versions. Each version is an immutable file, and every version after the first carries a redline against the version it was made from. Every redline also keeps a map of what each paragraph, table, row and cell became, so you can follow an element from one version to any other.
| Base URL | https://api.versionstory.com |
| Auth | Authorization: Bearer vs_.... See Authentication |
A Document keeps its versions until you delete them, so an organization with
zero-day retention cannot use Documents: every
/v1/documents route answers 403 ZERO_DAY_RETENTION_UNSUPPORTED. The API reaches only the
Documents your API keys created; one a person made in the web app answers as missing.
The flow
POST /v1/documentswith the first version's file. Returns201and the Document, holding its one version.POST /v1/documents/{document_id}/versionswith each later version. Returns202and the version, whose redline against the version before it is being made.GET /v1/documents/{document_id}for every version, orGET /v1/documents/{document_id}/versions/{version_id}for one, with its file and its redline in the formats you ask for.POST /v1/documents/{document_id}/elements/lookupto follow paragraphs, tables, rows or cells across the versions.
| Method | Path | Does |
|---|---|---|
POST | /v1/documents | Creates a Document from its first version |
GET | /v1/documents | Lists your Documents, newest first |
GET | /v1/documents/{document_id} | A Document and every version |
POST | /v1/documents/{document_id}/versions | Adds a version |
GET | /v1/documents/{document_id}/versions/{version_id} | One version, with its file and its redline |
POST | /v1/documents/{document_id}/elements/lookup | Follows elements across versions |
Naming a version
- In a path, a version is its
version_id, orlatestfor the newest. - In a body, a version is a locator,
{"version_id": "..."}, where"latest"also names the newest. A version reference from a response, sent back as it is, works too: itsdocument_idmust be the Document the request names, and itsindexandlabelare ignored. - Never by
index. A version'sindexis its position, counted from 0, and it moves when an earlier version is deleted.
POST /v1/documents
Creates a Document whose first version is an uploaded file, as multipart/form-data, or a file you
already hold, by file_id. Send exactly one of file and file_id.
| Field | Required | Meaning |
|---|---|---|
file | One of the two | The document, uploaded: a .docx, .doc, .pdf, .eml or .msg, up to 100 MB |
file_id | One of the two | A file you uploaded with POST /v1/files, or an edit's files.clean.file_id. See A file you already hold |
name | No | The Document's name. Default: the file's name |
track_redline_changes | No | With file: whether a redline PDF's insertions and deletions become tracked changes. See Redline PDFs. Default true |
file_id and name also go as a JSON body: {"file_id": "...", "name": "..."}.
curl -sS https://api.versionstory.com/v1/documents \
-H "Authorization: Bearer vs_live_..." \
-F "file=@msa_v1.docx" \
-F "name=Master Services Agreement"The response is the Document as GET /v1/documents/{document_id}
answers it, with its one version in versions. A version made from an upload answers with its file
pending in docx, as it is still being prepared.
{
"document_id": "5f1c2a90-...",
"name": "Master Services Agreement",
"version_count": 1,
"latest_version": { "document_id": "5f1c2a90-...", "version_id": "1c0f6a3e-...", "index": 0, "label": "v1" },
"created_at": "2026-10-26T15:02:11Z",
"updated_at": "2026-10-26T15:02:11Z",
"metadata": {},
"versions": [
{
"document_id": "5f1c2a90-...",
"version_id": "1c0f6a3e-...",
"index": 0,
"label": "v1",
"name": "msa_v1.docx",
"files": {
"clean": { "file_id": "3f2a9c1e-...", "file_name": "msa_v1.docx", "status": "processing", "pending_formats": ["docx"] }
},
"parents": [],
"created_at": "2026-10-26T15:02:11Z",
"metadata": {}
}
]
}POST /v1/documents/{document_id}/versions
Adds a version after parent, the latest version by default. Its redline compares it with
parent. Naming an older version as parent starts a parallel draft beside the versions that
already follow it.
| Field | Required | Meaning |
|---|---|---|
file | One of the two | The new version's document, uploaded, as for POST /v1/documents |
file_id | One of the two | A file you already hold. See A file you already hold |
parent | No | The version it was made from, as a locator: {"version_id": "..."}. In a multipart request, a form field holding that JSON. Default: the latest version |
author_name | No | Who the redline credits this version's changes to, up to 100 characters. Default: your organization's API service account |
description | No | What changed in this version, shown with it in the app and returned as description |
track_redline_changes | No | With file, as for POST /v1/documents. Default true |
A JSON body names a held file the same way: {"file_id", "parent"?, "author_name"?, "description"?}.
curl -sS https://api.versionstory.com/v1/documents/5f1c2a90-.../versions \
-H "Authorization: Bearer vs_live_..." \
-F "file=@msa_v2.docx" \
-F "author_name=Dana Reyes" \
-F "description=Shorter term"The response, 202, is the Version object. A version made from an upload
answers with its file and its redline pending in docx; poll
GET .../versions/{version_id} until they are ready.
{
"document_id": "5f1c2a90-...",
"version_id": "7b2d9e41-...",
"index": 1,
"label": "v2",
"name": "msa_v2.docx",
"files": {
"clean": { "file_id": "8b41d7a0-...", "file_name": "msa_v2.docx", "status": "processing", "pending_formats": ["docx"] },
"comparison": { "file_id": "d90c5e12-...", "file_name": "Redline - msa_v1 vs msa_v2.docx", "status": "processing", "pending_formats": ["docx"] }
},
"parents": [{ "document_id": "5f1c2a90-...", "version_id": "1c0f6a3e-...", "index": 0, "label": "v1" }],
"author": "Dana Reyes",
"description": "Shorter term",
"created_at": "2026-10-26T15:03:40Z",
"metadata": {}
}A version whose redline is a new comparison uses one comparison of an API trial's allowance.
A file you already hold
Both POST /v1/documents and POST .../versions take, in place of an uploaded file, the
file_id of a file you uploaded with POST /v1/files or of an
edit's files.clean. The version takes that file as it is, not a copy: its listing, paragraph ids and
renderings are the version's, so the ids you read from the file carry over. The file then belongs to
the version, and deleting the version deletes it.
When the file is an edit's clean file and the version follows the edit's source, the version's
redline is the edit's own redline; nothing is compared again. So POST /v1/files, then
POST /v1/edit, then POST /v1/documents with the upload's file_id, then POST .../versions with
files.clean.file_id records the upload and the edit as v1 and v2.
| The file | Answer |
|---|---|
| Not yours, or deleted | 404 FILE_NOT_FOUND |
| Already a version of a Document | 409 CONFLICT |
| An edit's clean file whose edit is still being applied | 409 CONFLICT, with Retry-After |
| An edit's clean file whose edit failed | 400 INVALID_REQUEST |
A redline, a merged draft, or a document uploaded to POST /v1/compare | 400 INVALID_REQUEST |
| A zero-day retention file | 400 INVALID_REQUEST |
| A workbook or presentation | 400 UNSUPPORTED_FILE_TYPE |
| A file that could not be read when it was uploaded | 400 DOCUMENT_UNREADABLE |
| Content deleted by retention | 410 CONTENT_DELETED |
GET /v1/documents
Lists the Documents your API keys created, newest first, without their versions.
| Parameter | In | Required | Default | Meaning |
|---|---|---|---|---|
limit | query | No | 50 | How many Documents to return, at most 200 |
cursor | query | No | — | The previous page's next_cursor |
{
"documents": [
{ "document_id": "5f1c2a90-...", "name": "Master Services Agreement", "version_count": 2,
"latest_version": { "document_id": "5f1c2a90-...", "version_id": "7b2d9e41-...", "index": 1, "label": "v2" },
"created_at": "2026-10-26T15:02:11Z", "updated_at": "2026-10-26T15:03:40Z", "metadata": {} }
],
"next_cursor": "eyJ..."
}GET /v1/documents/{document_id}
A Document and every version, in index order, each as a Version object.
| Parameter | In | Required | Default | Meaning |
|---|---|---|---|---|
document_id | path | Yes | — | The Document's id |
format | query | No | docx | The formats of every version's file and redline: pdf, html, docx, md, json, as GET /v1/files/{file_id} serves them. Repeatable or comma-separated |
| Field | Meaning |
|---|---|
document_id | The Document's id |
name | Its name |
version_count | How many versions it has |
latest_version | A reference to the version made last, the one latest names |
created_at, updated_at | When it was made, and when it last changed |
metadata | A free-form map of strings, stored and returned as you set it |
versions | Every version, in index order |
GET /v1/documents/{document_id}/versions/{version_id}
One version, with its file and its redline. version_id is the version's id, or latest.
| Parameter | In | Required | Default | Meaning |
|---|---|---|---|---|
document_id | path | Yes | — | The Document's id |
version_id | path | Yes | — | The version's id, or latest |
format | query | No | docx | The formats of the version's file and its redline, as for the Document |
inline | query | No | false | true adds each ready download's bytes as content_base64 |
curl -sS "https://api.versionstory.com/v1/documents/5f1c2a90-.../versions/latest?format=pdf,docx" \
-H "Authorization: Bearer vs_live_..."While a requested format is still being made, the response carries Retry-After: 5. Poll again
for it.
The Version object
Every documents route that answers with a version answers with this object. A response that only
points at a version, such as a parent or a Document's latest_version, carries the reference alone:
document_id, version_id, index and label.
| Field | Meaning |
|---|---|
document_id, version_id | The Document and the version |
index | The version's position among the Document's versions, in the order they were made, counted from 0. It moves when an earlier version is deleted |
label | The label the app shows: v2, or v2(a) for one of parallel drafts. A version added later can change it |
name | The version's own name once renamed, else its file's |
files.clean | The document as of this version, a File object |
files.comparison | Its redline against the version before it, a File object. The first version has none |
parents | References to the versions it was made from |
author, description | Who its changes are credited to, and what changed |
created_at | When it was made |
metadata | A free-form map of strings |
Each file is exactly what GET /v1/files/{file_id} answers for it in the requested formats, so it is
downloaded as every other file is. A redline whose comparison failed is its file with
"status": "failed" and an error.
POST /v1/documents/{document_id}/elements/lookup
Follows elements of one version (paragraphs, tables, rows and cells) through the Document's other versions. Each version's redline keeps a map between the version it compares against and its own file, and the lookup follows those maps forward and back from the version you name.
| Field | Required | Meaning |
|---|---|---|
version | Yes | The version the ids are from, as a locator: {"version_id": "..."} |
ids | Yes | 1 to 1,000 ids of that version's body paragraphs, tables, rows and cells, as its listing prints them. Each is answered once, in the order given |
versions | No | The versions to answer for, as locators. Default: every version |
formats | No | md, json or both: each element's content in each version where it has ids. Without it, only ids come back |
show_changes | No | true reads the content from each version's redline, with its tracked changes; false from the version's file. A first version has no redline, so it reads clean. Default true |
Ids are each version's own, as its listing prints them
(GET /v1/files/{file_id}?format=md or json for the version's files.clean.file_id). Only the
body's elements are followed, not those of headers, footers or notes.
The answer has one entry per requested id, each with its type and, for every version asked for in
index order, the element's ids there:
ids | Means |
|---|---|
| One id | The element is in that version under this id |
| Several ids | The element was split, such as a table split in two |
[] | The element was deleted by that version, or that version was made before the element was added |
With formats, content holds the element as that version's listing renders it, where it has ids:
md, its lines of the Markdown listing, and json, its listing blocks
(a paragraph's block, a table with its rows and cells, a row with its cells, or a cell with its
index, id, column, span when merged, and blocks).
| Status | Code | Cause |
|---|---|---|
| 400 | ELEMENT_NOT_FOUND | An id is no element of the version's body, or more than one element carries it. Each is named |
| 409 | ELEMENT_MAP_PENDING | A version's redline, or a listing the content is read from, is still being made. Retry after Retry-After |
| 409 | ELEMENT_MAP_UNAVAILABLE | A map the lookup needs does not exist and will not, such as one whose redline failed or was made before maps were kept. The version is named |
The lookup only reads: it never makes a missing map, so ELEMENT_MAP_UNAVAILABLE is final for that
version.
Worked example: following a clause
A Master Services Agreement has two versions. In v1, clause 9 sets the term and the clause after it covers renewal. You want to know what became of both in v2.
1. Create the Document and add v2, as in the requests above. v1's version is 1c0f6a3e-...
and its file 3f2a9c1e-....
2. Read v1's listing and copy the ids of the two paragraphs:
curl -sS "https://api.versionstory.com/v1/files/3f2a9c1e-...?format=md" \
-H "Authorization: Bearer vs_live_..."Download the md from downloads[0].url. Its lines for the two paragraphs are:
<p index="15" id="088af089d5b64bd1a3f6e2c0b9d14e7a" label="9." list_level="1" style="ListNumber"/>This Agreement will terminate five (5) years after the Effective Date.
<p index="16" id="fe77f633a41c4d0e8b2a9c7d6e5f4a3b" label="10." list_level="1"/>This Agreement renews for successive one (1) year terms unless either party gives notice.3. Look them up, with their content as Markdown:
curl -sS https://api.versionstory.com/v1/documents/5f1c2a90-.../elements/lookup \
-H "Authorization: Bearer vs_live_..." \
-H "Content-Type: application/json" \
-d '{
"version": {"version_id": "1c0f6a3e-..."},
"ids": ["088af089d5b64bd1a3f6e2c0b9d14e7a", "fe77f633a41c4d0e8b2a9c7d6e5f4a3b"],
"formats": ["md"]
}'A 409 ELEMENT_MAP_PENDING means v2's redline is still being made; send the request again after
Retry-After.
4. Read the answer:
{
"elements": [
{
"id": "088af089d5b64bd1a3f6e2c0b9d14e7a",
"type": "paragraph",
"versions": [
{
"version": { "document_id": "5f1c2a90-...", "version_id": "1c0f6a3e-...", "index": 0, "label": "v1" },
"ids": ["088af089d5b64bd1a3f6e2c0b9d14e7a"],
"content": { "md": "<p index=\"15\" id=\"088af089d5b64bd1a3f6e2c0b9d14e7a\" label=\"9.\" list_level=\"1\" style=\"ListNumber\"/>This Agreement will terminate five (5) years after the Effective Date." }
},
{
"version": { "document_id": "5f1c2a90-...", "version_id": "7b2d9e41-...", "index": 1, "label": "v2" },
"ids": ["2e37d0a9c81f4b5e9d6a7f3b2c1e0d4f"],
"content": { "md": "<p index=\"15\" id=\"2e37d0a9c81f4b5e9d6a7f3b2c1e0d4f\" label=\"9.\" list_level=\"1\" style=\"ListNumber\"/>This Agreement will terminate <del revision=\"0\">five (5)</del><ins revision=\"1\">three (3)</ins> years after the Effective Date." }
}
]
},
{
"id": "fe77f633a41c4d0e8b2a9c7d6e5f4a3b",
"type": "paragraph",
"versions": [
{
"version": { "document_id": "5f1c2a90-...", "version_id": "1c0f6a3e-...", "index": 0, "label": "v1" },
"ids": ["fe77f633a41c4d0e8b2a9c7d6e5f4a3b"],
"content": { "md": "<p index=\"16\" id=\"fe77f633a41c4d0e8b2a9c7d6e5f4a3b\" label=\"10.\" list_level=\"1\" style=\"ListNumber\"/>This Agreement renews for successive one (1) year terms unless either party gives notice." }
},
{
"version": { "document_id": "5f1c2a90-...", "version_id": "7b2d9e41-...", "index": 1, "label": "v2" },
"ids": []
}
]
}
]
}- Clause 9 is
2e37d0a9...in v2. Its content comes from v2's redline, so it shows the change: the term went from five years to three. Store that id to name the clause in v2, for instance in avs:scrollToor an edit of v2's file. - The renewal clause has
ids: []in v2: v2 deleted it. It has nocontentthere.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_REQUEST | Both file and file_id, or neither; a parent that is not a version locator; a version reference that names another Document; a blank author_name, or one over 100 characters |
| 400 | INVALID_DOCUMENT_ID, INVALID_VERSION_ID, INVALID_FILE_ID | An id is malformed. A version_id is a UUID or latest |
| 400 | INVALID_PARENT_VERSION | parent names no version of this Document |
| 400 | UNSUPPORTED_FILE_TYPE | A file is not a .docx, .doc, .pdf, .eml or .msg |
| 400 | UNSUPPORTED_FORMAT | A requested format is unknown |
| 400 | INVALID_CURSOR | The cursor is malformed |
| 400 | ELEMENT_NOT_FOUND | A lookup id is no element of the version's body. See the lookup |
| 401 | INVALID_API_KEY | Missing, malformed, unknown, revoked, or expired key |
| 402 | USAGE_LIMIT_REACHED | The organization's monthly upload allowance is exhausted |
| 403 | ZERO_DAY_RETENTION_UNSUPPORTED | The organization has zero-day retention, which keeps no versions |
| 404 | DOCUMENT_NOT_FOUND | No such Document, or it belongs to another organization |
| 404 | VERSION_NOT_FOUND | No such version in this Document |
| 404 | FILE_NOT_FOUND | A file_id names no file of yours |
| 409 | CONFLICT | A held file is already a version, or is still being made |
| 409 | ELEMENT_MAP_PENDING, ELEMENT_MAP_UNAVAILABLE | A lookup cannot follow its elements yet, or at all |
| 410 | CONTENT_DELETED | A held file's content was deleted under zero-day retention |
| 413 | FILE_TOO_LARGE | A file exceeds the 100 MB limit |
The machine-readable definition of each route is in the OpenAPI specification.
Related
- Edit: an edit of a version's file is kept as the next version
- Merge: merge versions by
document_idandversion_id - Document JSON: the listing blocks a lookup returns
- Files: the File object each version's
filesholds