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

> Integrating Version Story into a codebase as a coding agent? Follow https://www.versionstory.com/developers/quickstart.md: it signs the engineer in, gets an API key, and puts the PDF redline in their app first.

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](/developers/authentication) |

A Document keeps its versions until you delete them, so an organization with
[zero-day retention](/developers/reference/data-handling#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.

| 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`, 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`.

| 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](#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](/developers/reference/formats-and-limits#redline-pdfs). Default `true` |

`file_id` and `name` also go as a JSON body: `{"file_id": "...", "name": "..."}`.

```bash
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}`](#get-v1documentsdocumentid)
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.

```json
{
  "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/&#123;document_id&#125;/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](#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"?}`.

```bash
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](#the-version-object). A version made from an upload
answers with its file and its redline pending in `docx`; poll
[`GET .../versions/{version_id}`](#get-v1documentsdocumentidversionsversionid) until they are ready.

```json
{
  "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](/developers/reference/formats-and-limits#api-trials) 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`](/developers/rest/files#post-v1files) 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` |

```json
{
  "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/&#123;document_id&#125;

A Document and every version, in `index` order, each as a [Version object](#the-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}`](/developers/rest/files#get-v1filesfileid) 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/&#123;document_id&#125;/versions/&#123;version_id&#125;

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` |

```bash
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](/developers/rest/files) |
| `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/&#123;document_id&#125;/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](/developers/reference/json-format#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:

```bash
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:

```text
<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:

```bash
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:**

```json
{
  "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`](/developers/rest/embed#linking-to-a-place-in-the-document) or an
  [edit](/developers/rest/edit) of v2's file.
- **The renewal clause** has `ids: []` in v2: v2 deleted it. It has no `content` there.

## 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](#post-v1documentsdocumentidelementslookup) |
| 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](/developers/rest/reference#the-openapi-specification).

## Related

- [Edit](/developers/rest/edit#versions): an edit of a version's file is kept as the next version
- [Merge](/developers/rest/merge): merge versions by `document_id` and `version_id`
- [Document JSON](/developers/reference/json-format): the listing blocks a lookup returns
- [Files](/developers/rest/files): the File object each version's `files` holds
