Skip to content

Documents

Keep a document's versions in one history, with a redline between each version and the one before it, and follow a paragraph, table, row or cell from one version to another.

View as Markdown

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 URLhttps://api.versionstory.com
AuthAuthorization: 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

  1. POST /v1/documents with the first version's file. Returns 201 and the Document, holding its one version.
  2. POST /v1/documents/{document_id}/versions with each later version. Returns 202 and the version, whose redline against the version before it is being made.
  3. GET /v1/documents/{document_id} for every version, or GET /v1/documents/{document_id}/versions/{version_id} for one, with its file and its redline in the formats you ask for.
  4. POST /v1/documents/{document_id}/elements/lookup to follow paragraphs, tables, rows or cells across the versions.
MethodPathDoes
POST/v1/documentsCreates a Document from its first version
GET/v1/documentsLists your Documents, newest first
GET/v1/documents/{document_id}A Document and every version
POST/v1/documents/{document_id}/versionsAdds a version
GET/v1/documents/{document_id}/versions/{version_id}One version, with its file and its redline
POST/v1/documents/{document_id}/elements/lookupFollows elements across versions

Naming a version

  • In a path, a version is its version_id, or latest for 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: its document_id must be the Document the request names, and its index and label are ignored.
  • Never by index. A version's index is 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.

FieldRequiredMeaning
fileOne of the twoThe document, uploaded: a .docx, .doc, .pdf, .eml or .msg, up to 100 MB
file_idOne of the twoA file you uploaded with POST /v1/files, or an edit's files.clean.file_id. See A file you already hold
nameNoThe Document's name. Default: the file's name
track_redline_changesNoWith 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.

FieldRequiredMeaning
fileOne of the twoThe new version's document, uploaded, as for POST /v1/documents
file_idOne of the twoA file you already hold. See A file you already hold
parentNoThe 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_nameNoWho the redline credits this version's changes to, up to 100 characters. Default: your organization's API service account
descriptionNoWhat changed in this version, shown with it in the app and returned as description
track_redline_changesNoWith 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 fileAnswer
Not yours, or deleted404 FILE_NOT_FOUND
Already a version of a Document409 CONFLICT
An edit's clean file whose edit is still being applied409 CONFLICT, with Retry-After
An edit's clean file whose edit failed400 INVALID_REQUEST
A redline, a merged draft, or a document uploaded to POST /v1/compare400 INVALID_REQUEST
A zero-day retention file400 INVALID_REQUEST
A workbook or presentation400 UNSUPPORTED_FILE_TYPE
A file that could not be read when it was uploaded400 DOCUMENT_UNREADABLE
Content deleted by retention410 CONTENT_DELETED

GET /v1/documents

Lists the Documents your API keys created, newest first, without their versions.

ParameterInRequiredDefaultMeaning
limitqueryNo50How many Documents to return, at most 200
cursorqueryNo—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.

ParameterInRequiredDefaultMeaning
document_idpathYes—The Document's id
formatqueryNodocxThe 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
FieldMeaning
document_idThe Document's id
nameIts name
version_countHow many versions it has
latest_versionA reference to the version made last, the one latest names
created_at, updated_atWhen it was made, and when it last changed
metadataA free-form map of strings, stored and returned as you set it
versionsEvery 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.

ParameterInRequiredDefaultMeaning
document_idpathYes—The Document's id
version_idpathYes—The version's id, or latest
formatqueryNodocxThe formats of the version's file and its redline, as for the Document
inlinequeryNofalsetrue 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.

FieldMeaning
document_id, version_idThe Document and the version
indexThe 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
labelThe label the app shows: v2, or v2(a) for one of parallel drafts. A version added later can change it
nameThe version's own name once renamed, else its file's
files.cleanThe document as of this version, a File object
files.comparisonIts redline against the version before it, a File object. The first version has none
parentsReferences to the versions it was made from
author, descriptionWho its changes are credited to, and what changed
created_atWhen it was made
metadataA 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.

FieldRequiredMeaning
versionYesThe version the ids are from, as a locator: {"version_id": "..."}
idsYes1 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
versionsNoThe versions to answer for, as locators. Default: every version
formatsNomd, json or both: each element's content in each version where it has ids. Without it, only ids come back
show_changesNotrue 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:

idsMeans
One idThe element is in that version under this id
Several idsThe 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).

StatusCodeCause
400ELEMENT_NOT_FOUNDAn id is no element of the version's body, or more than one element carries it. Each is named
409ELEMENT_MAP_PENDINGA version's redline, or a listing the content is read from, is still being made. Retry after Retry-After
409ELEMENT_MAP_UNAVAILABLEA 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 a vs:scrollTo or an edit of v2's file.
  • The renewal clause has ids: [] in v2: v2 deleted it. It has no content there.

Errors

StatusCodeCause
400INVALID_REQUESTBoth 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
400INVALID_DOCUMENT_ID, INVALID_VERSION_ID, INVALID_FILE_IDAn id is malformed. A version_id is a UUID or latest
400INVALID_PARENT_VERSIONparent names no version of this Document
400UNSUPPORTED_FILE_TYPEA file is not a .docx, .doc, .pdf, .eml or .msg
400UNSUPPORTED_FORMATA requested format is unknown
400INVALID_CURSORThe cursor is malformed
400ELEMENT_NOT_FOUNDA lookup id is no element of the version's body. See the lookup
401INVALID_API_KEYMissing, malformed, unknown, revoked, or expired key
402USAGE_LIMIT_REACHEDThe organization's monthly upload allowance is exhausted
403ZERO_DAY_RETENTION_UNSUPPORTEDThe organization has zero-day retention, which keeps no versions
404DOCUMENT_NOT_FOUNDNo such Document, or it belongs to another organization
404VERSION_NOT_FOUNDNo such version in this Document
404FILE_NOT_FOUNDA file_id names no file of yours
409CONFLICTA held file is already a version, or is still being made
409ELEMENT_MAP_PENDING, ELEMENT_MAP_UNAVAILABLEA lookup cannot follow its elements yet, or at all
410CONTENT_DELETEDA held file's content was deleted under zero-day retention
413FILE_TOO_LARGEA file exceeds the 100 MB limit

The machine-readable definition of each route is in the OpenAPI specification.

  • Edit: an edit of a version's file is kept as the next version
  • Merge: merge versions by document_id and version_id
  • Document JSON: the listing blocks a lookup returns
  • Files: the File object each version's files holds