Two endpoints do the work: one submits a pair of documents, the other polls for the finished redline and hands back signed download links. A third, for organizations that opt into zero-day retention, deletes a comparison as soon as you have its result.
| Base URL | https://api.versionstory.com |
| Auth | Authorization: Bearer vs_.... See Authentication |
Every non-2xx response uses the same envelope. Send an X-Request-Id header to correlate with
our logs; if you don't, one is minted and echoed back on the response.
{ "error": { "code": "INVALID_API_KEY", "message": "...", "request_id": "..." } }Why the PDF
Engineers new to legal tend to see a redline as a diff. Lawyers know it is a deliverable their clients judge them by. So the first thing to build is the PDF redline, shown where your lawyers work.
- It is the record. Lawyers make the record. A litigator asks that the record reflect what happened; a transactional lawyer builds the record of a deal as it goes: what was sent, when, and what changed. A PDF redline is that record at one moment, the changes between two drafts exactly as they stood, and it reads the same in five years. A view your app draws for itself is not a record: it changes whenever your code does.
- It looks right everywhere. A PDF is a portable document: the same pages in your app, in an agent's chat, in a Slack thread, attached to an email, in a client portal, and on paper. There is no viewer to build, and nothing to redraw when your stack changes.
- It is what lawyers review. Clean, crisp pages, with page numbers to cite on a call: page 7, the second paragraph.
- It is what clients see. A redline sent to a client carries the firm's name. As Mitch Duncombe of Vector Legal put it: "Beautiful redlines matter to a law firm as a business. When we present to a company, we want polished deliverables, and Version Story's PDF comparisons embed right in our client portal."
Each other format has its own job: the embedded HTML for working through the changes one by one, the Word file for negotiating on, the Markdown for a model to read, and the JSON for code that acts on the changes.
The flow
POST /v1/comparewith both files. Returns202and acomparison_idimmediately.GET /v1/compare/{comparison_id}?format=pdfuntilstatusisready, then fetchfile.downloads[].url.
Generation is asynchronous. Neither call blocks: the first stages the work, and the second is a single-shot readiness check you repeat.
POST /v1/compare
Submits two documents for comparison: multipart/form-data with two file fields and optional
fields.
| Field | Required | Meaning |
|---|---|---|
original | Yes | The base document, the earlier version |
modified | Yes | The revised document, the later version |
author | No | Name to attribute the redline's tracked changes to. Default: the revised document's file name |
preserve_track_changes | No | true keeps the tracked changes already in both documents as they are; see Keeping tracked changes. Default false |
show_formatting_changes | No | true also tracks formatting and table layout changes; see Formatting changes. Default false |
track_redline_changes | No | true turns a redline PDF's insertions and deletions into tracked changes, so the comparison reads it as its revised text; see Redline PDFs. Default true |
The direction matters: the redline describes what changed in going from original to
modified. Swapping them gives you insertions where you expected deletions.
curl -sS https://api.versionstory.com/v1/compare \
-H "Authorization: Bearer vs_live_..." \
-F "original=@nda_v1.docx" \
-F "modified=@nda_v2.docx" \
-F "author=Dana Reyes"What you can compare
- Documents:
.docx,.doc, or.pdf, in any combination. Sources that are not already.docxare converted first, so.docand.pdfcomparisons spend longer inprocessing. - Presentations: two
.pptxfiles make a native presentation comparison, whose redline is itself a.pptx(see Formats). A presentation can only be compared with another presentation; a.pptxpaired with anything else is refused. - Workbooks: two
.xlsxfiles make a native Excel comparison, whose redline is itself a.xlsx. A workbook can only be compared with another workbook; a.xlsxpaired with anything else is refused. - Each file can be up to 100 MB.
- Both files may share a name; two revisions of
contract.docxis a normal case, and the revised side is disambiguated automatically.
Attributing the changes
author becomes the author on every insertion and deletion in the Word redline: what Word shows
in the reviewing pane and in each change's tooltip. Without it, the changes are attributed to the
revised file's name. Pass the name your reviewers should see instead.
Keeping tracked changes
By default the comparison treats every tracked change in both documents as accepted, as Word's
Compare and Litera do, so proposed changes become part of the text being compared. Set
preserve_track_changes=true to keep them as proposals:
- Tracked changes in either file stay. Each tracked change already in
originalor inmodifiedis kept as it is, under its own author and date. - Only real differences are added. What
modifiedchanges beyond its own tracked changes, such as edits made with Track Changes off, is added as new tracked changes underauthor. Text that reads the same in both is not marked again.
The usual case is a counterparty's draft. It arrives with their tracked changes, and you edit it,
with Track Changes on or off. Send their draft as original and your copy as modified. The
redline shows their changes under their names, your tracked changes under yours, and your untracked
edits under author. Accepting every change gives your copy with its changes accepted; rejecting
every change gives the draft as it was before anyone's tracked changes.
curl -sS https://api.versionstory.com/v1/compare \
-H "Authorization: Bearer vs_live_..." \
-F "original=@counterparty_draft.docx" \
-F "modified=@our_edits.docx" \
-F "author=Dana Reyes" \
-F "preserve_track_changes=true"It applies to Word documents; a pair of presentations or workbooks with it set is refused with
INVALID_REQUEST.
Formatting changes
By default the redline tracks what the text says, not how it looks: a change of font, size, bold,
color, alignment, spacing or table layout is not marked. Set show_formatting_changes=true to
track those changes too.
| Applies to | |
|---|---|
| Word documents | Yes |
| Presentations and workbooks | No: the request is refused with INVALID_REQUEST |
With preserve_track_changes=true | No: the field has no effect |
| A document converted from a PDF | No: the field has no effect |
Response: 202 Accepted
{
"comparison_id": "cmp_MTp2cy0xOnZtLTE",
"status": "processing",
"file": {
"file_id": "3f2c...",
"file_name": "Redline - nda_v1 to nda_v2.docx",
"status": "processing",
"pending_formats": ["docx"]
}
}comparison_id is opaque and stable. Store it: it is the handle to the comparison and its
renderings. file is the Word redline as a file, the same object
GET /v1/files/{file_id} answers with, so you can also comment on its changes
or edit it. A presentation's redline is a file too, served as pptx and pdf, and a workbook's as
xlsx; neither has a listing, so neither can be edited.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_REQUEST | A file field is missing, empty, or has no file name, or preserve_track_changes or show_formatting_changes is set for presentations or workbooks |
| 400 | UNSUPPORTED_FILE_TYPE | An extension is not .docx, .doc, .pdf, .pptx or .xlsx, or only one of the two files is a .pptx or a .xlsx |
| 400 | DOCUMENT_UNREADABLE | A .docx source is not a readable Word file: corrupt, or encrypted |
| 401 | INVALID_API_KEY | Missing, malformed, unknown, revoked, or expired key |
| 402 | USAGE_LIMIT_REACHED | The organization's monthly upload allowance is exhausted |
| 403 | API_KEY_ORG_MISMATCH | The key's service account no longer belongs to the key's organization |
| 403 | TRIAL_COMPARISON_LIMIT_REACHED | The organization's API trial allowance is used up |
| 413 | FILE_TOO_LARGE | A file exceeds the 100 MB limit |
| 429 | TRIAL_RATE_LIMITED | The trial's per-minute rate is exceeded; wait for Retry-After seconds |
Usage and trial checks run before anything is created, so a refused request costs nothing.
GET /v1/compare/{comparison_id}
Reports whether the redline exists yet, and returns signed download links once it does. Safe to call as often as you like: it creates nothing.
| Parameter | In | Required | Default | Meaning |
|---|---|---|---|---|
comparison_id | path | Yes | — | The id from POST /v1/compare |
format | query | No | docx (pptx for presentations, xlsx for workbooks) | Which renderings to return. Name pdf to show the redline |
Formats
format is repeatable, and also accepts a comma-separated list, so one call can ask for several
renderings: ?format=pdf&format=docx and ?format=pdf,docx are equivalent. A request that names
no format gets the Word file, which is for negotiating on, not for showing.
format | Artifact | Use it for |
|---|---|---|
pdf | PDF rendering of the redline | Showing a person the redline. Start here |
html | The interactive redline as one self-contained HTML file. See Embed redlines | An interactive view beside the PDF: change cards, filters, Previous and Next |
docx | Word document with real tracked changes (the default) | A lawyer to accept, reject and send on in Word |
pdf_changed_pages_only | PDF containing only the pages that changed | A quick review of a long agreement |
md | The redline as Markdown, with every change marked inline. See Redline format | A model to read: summaries, questions about the changes, checks against a playbook |
json | The redline as structured data. See Document JSON | Code that acts on the changes: counting them, routing a document, linking a change to your own records. Not for display |
Do not build a redline viewer from json. It describes the document's structure and its changes,
not how the document is laid out in pages, so a view drawn from it re-implements Word's layout and
still shows less than the PDF and the HTML redline do. Show the PDF, and embed the HTML where the
view must be interactive.
A presentation comparison offers pptx (a presentation with the changes marked, the default) and
pdf, and an Excel comparison xlsx (a workbook with the changes marked) only. Asking either for
any other format is an UNSUPPORTED_FORMAT error.
curl -sS "https://api.versionstory.com/v1/compare/cmp_MTp2cy0xOnZtLTE?format=pdf,docx" \
-H "Authorization: Bearer vs_live_..."Response: ready
file holds the redline's downloads, one per requested format, in the order you asked for them.
{
"comparison_id": "cmp_MTp2cy0xOnZtLTE",
"status": "ready",
"file": {
"file_id": "3f2c...",
"file_name": "Redline - nda_v1 to nda_v2.docx",
"status": "ready",
"downloads": [
{
"format": "pdf",
"url": "https://documents.versionstory.com/...",
"file_name": "Redline - nda_v1 to nda_v2.pdf",
"expires_at": "2026-10-22T22:00:00Z"
},
{
"format": "docx",
"url": "https://documents.versionstory.com/...",
"file_name": "Redline - nda_v1 to nda_v2.docx",
"expires_at": "2026-10-22T22:00:00Z"
}
]
}
}Each url is signed and needs no Authorization header: fetch it directly, saving the response
as a binary file. It stops working at expires_at, one hour after it was issued, so store the
comparison_id or the file, never the link. Poll again at any time for fresh links; the redline
itself does not expire, unless the organization uses zero-day retention.
curl -sS --fail -o redline.pdf "<file.downloads[0].url>"status is the comparison's, and file.status the redline file's. A failed comparison has no
file.
Response: still generating
Returned with Retry-After: 1. Honor it rather than polling tighter; a comparison is not made
faster by being asked about more often.
Renderings do not all finish together: the PDF and the Markdown trail the Word document. Rather
than withhold what already exists, the response returns every format that is ready and names the
rest in file.pending_formats. status is ready only once every format you asked for is
available.
{
"comparison_id": "cmp_MTp2cy0xOnZtLTE",
"status": "processing",
"file": {
"file_id": "3f2c...",
"file_name": "Redline - nda_v1 to nda_v2.docx",
"status": "processing",
"downloads": [
{
"format": "docx",
"url": "https://documents.versionstory.com/...",
"file_name": "Redline - nda_v1 to nda_v2.docx",
"expires_at": "2026-10-22T22:00:00Z"
}
],
"pending_formats": ["pdf"]
}
}Response: failed
Terminal. The comparison will not succeed on its own, and waiting longer changes nothing; submit a new comparison to retry. There are two shapes.
The redline itself failed. error says why, and there is no file:
{
"comparison_id": "cmp_MTp2cy0xOnZtLTE",
"status": "failed",
"error": {
"code": "DOCUMENT_PROTECTED",
"upstream_code": "ONE_OR_MORE_DOCUMENTS_ARE_PROTECTED"
}
}error.code | Meaning |
|---|---|
DOCUMENT_PROTECTED | A source document is password-protected or permission-restricted |
DOCUMENT_UNREADABLE | A source document could not be opened: corrupt or malformed |
DOCUMENT_CONVERSION_FAILED | A .doc or .pdf source could not be converted to .docx |
SOURCE_UPLOAD_FAILED | A source document did not reach storage intact |
PDF_RENDER_FAILED | The redline generated, but its PDF rendering did not |
COMPARISON_FAILED | Catch-all; upstream_code carries the specific pipeline code |
The redline succeeded but a rendering of it did not. Everything that was produced is still in
file.downloads, and the formats that will never arrive are named in file.failed_formats.
Usable files are usable; only the named formats are missing.
Either way, a failed comparison is reported as 200 OK with "status": "failed", because the
request succeeded: it correctly told you what is not going to happen. Non-2xx status codes are
reserved for problems with the request itself.
Request errors
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_COMPARISON_ID | The id is malformed, or is a mrg_ merge id |
| 400 | UNSUPPORTED_FORMAT | A requested format is unknown, or not available for this kind of comparison |
| 403 | TRIAL_WORD_DOWNLOADS_EXCLUDED | docx was requested, and the organization's API trial excludes Word downloads |
| 404 | COMPARISON_NOT_FOUND | No such comparison, or it belongs to another organization |
| 410 | CONTENT_DELETED | The comparison's content was deleted under zero-day retention |
COMPARISON_NOT_FOUND covers both "doesn't exist" and "not yours" on purpose, so that ids cannot
be probed for existence.
Fields kept for existing integrations
Responses also carry redline_file_id, downloads, pending_formats and failed_formats at the
top level. They are deprecated, and marked so in the OpenAPI specification. New code reads file:
redline_file_id is file.file_id (absent for a presentation or workbook comparison), and the
top-level downloads serves every format file does, with two differences:
jsonthere is the comparison's older JSON, kept for the integrations built on it. Thejsoninfileis the redline's listing.expires_atthere keeps its older form, with+00:00and fractional seconds. Every other time ends inZ.
Commenting on the redline's changes
A Word comparison's redline is also a file you can read and edit, named by file.file_id. To
annotate its tracked changes, such as noting why each change was made, comment on each change by
its index, with no download and upload:
GET /v1/files/{file_id}?format=jsonfor the redline'sfile.file_idlists its tracked changes inrevisions, each with itsindex,type,authorand the start of itstext. The format is pending until the redline exists.POST /v1/editwith thatfile_idand oneadd_commentper change:{"type": "add_comment", "revision": {"index": 2}, "content": "..."}.- Download the result's
files.comparison: the comparison's tracked changes with your comments beside them.
curl -sS https://api.versionstory.com/v1/edit \
-H "Authorization: Bearer vs_live_..." \
-H "Content-Type: application/json" \
-d '{"file_id": "3f2c...", "schema_version": 1, "author": "Dana Reyes",
"wait": true, "outputs": ["pdf", "docx"],
"edits": [
{"type": "add_comment", "revision": {"index": 0}, "content": "Section 1 now defines the services."},
{"type": "add_comment", "revision": {"index": 2}, "content": "45 days matches the client AP cycle."}]}'Each comment covers what its change changed: the text inserted, deleted or moved (at the move's
destination), or the paragraph, row or cell whose formatting changed. See
add_comment.
Zero-day retention
By default, a comparison's documents and redline are kept, and you can come back for fresh download links at any time. Organizations that would rather keep nothing can turn on zero-day retention in the web app, on the Developer page's Overview tab.
With it on, every new POST /v1/compare comparison is deleted permanently once you have its
result. The source documents, the redline, and every rendering of it are removed; only a record
that the comparison existed remains. Deletion happens at whichever comes first:
- One hour after the comparison finishes. Each download's
expires_atis that deadline, so it tells you exactly how long you have. - When you acknowledge the download, with the endpoint below. Call it as soon as you have saved what you need.
After deletion, GET /v1/compare/{comparison_id} answers 410 CONTENT_DELETED.
The setting is stamped onto each comparison when it is created: turning it on or off never changes comparisons that already exist. Merges, edits, uploaded files and created documents are deleted on the same terms, each with its own acknowledge call; see Data handling.
POST /v1/compare/{comparison_id}/acknowledge
Tells Version Story you have downloaded the result, so it can delete the comparison now rather than at the deadline. No body.
curl -sS -X POST https://api.versionstory.com/v1/compare/cmp_MTp2cy0xOnZtLTE/acknowledge \
-H "Authorization: Bearer vs_live_..."{ "comparison_id": "cmp_MTp2cy0xOnZtLTE", "status": "deletion_queued" }The 202 means deletion is queued; it completes moments later.
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_COMPARISON_ID | The id is malformed |
| 404 | COMPARISON_NOT_FOUND | No such comparison, or it belongs to another organization |
| 409 | CONFLICT | The comparison is still processing, or was created without zero-day retention |
| 410 | CONTENT_DELETED | The comparison has already been deleted |
Polling in practice
A reasonable client submits, then polls on the interval the server suggests, with a ceiling on
total wait rather than on attempts. Most .docx comparisons finish in seconds; .pdf and .doc
sources take longer because they are converted first.
const BASE = "https://api.versionstory.com";
const headers = { Authorization: `Bearer ${process.env.VERSION_STORY_API_KEY}` };
async function waitForRedline(comparisonId, { formats = ["pdf"], timeoutMs = 10 * 60_000 } = {}) {
const deadline = Date.now() + timeoutMs;
const query = new URLSearchParams({ format: formats.join(",") });
while (Date.now() < deadline) {
const response = await fetch(`${BASE}/v1/compare/${comparisonId}?${query}`, { headers });
const body = await response.json();
if (!response.ok) {
throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);
}
if (body.status === "ready") return body.file.downloads;
if (body.status === "failed") {
throw new Error(`Comparison failed: ${body.error?.code ?? `no ${body.file.failed_formats.join(", ")}`}`);
}
const retryAfter = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
}
throw new Error("Timed out waiting for the redline");
}Related
- Embed redlines: the interactive HTML redline, beside the PDF
- Selective compare: one range of paragraphs against another, answered at once
- Merge: several people's edits to one document, in one draft
- Formats & limits: file types, size caps, trials