Skip to content

Compare

Submit two documents, poll for the redline, download it. The REST comparison endpoints in full.

View as Markdown

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

  1. POST /v1/compare with both files. Returns 202 and a comparison_id immediately.
  2. GET /v1/compare/{comparison_id}?format=pdf until status is ready, then fetch file.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.

FieldRequiredMeaning
originalYesThe base document, the earlier version
modifiedYesThe revised document, the later version
authorNoName to attribute the redline's tracked changes to. Default: the revised document's file name
preserve_track_changesNotrue keeps the tracked changes already in both documents as they are; see Keeping tracked changes. Default false
show_formatting_changesNotrue also tracks formatting and table layout changes; see Formatting changes. Default false
track_redline_changesNotrue 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 .docx are converted first, so .doc and .pdf comparisons spend longer in processing.
  • Presentations: two .pptx files make a native presentation comparison, whose redline is itself a .pptx (see Formats). A presentation can only be compared with another presentation; a .pptx paired with anything else is refused.
  • Workbooks: two .xlsx files make a native Excel comparison, whose redline is itself a .xlsx. A workbook can only be compared with another workbook; a .xlsx paired with anything else is refused.
  • Each file can be up to 100 MB.
  • Both files may share a name; two revisions of contract.docx is 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 original or in modified is kept as it is, under its own author and date.
  • Only real differences are added. What modified changes beyond its own tracked changes, such as edits made with Track Changes off, is added as new tracked changes under author. 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 documentsYes
Presentations and workbooksNo: the request is refused with INVALID_REQUEST
With preserve_track_changes=trueNo: the field has no effect
A document converted from a PDFNo: 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

StatusCodeCause
400INVALID_REQUESTA file field is missing, empty, or has no file name, or preserve_track_changes or show_formatting_changes is set for presentations or workbooks
400UNSUPPORTED_FILE_TYPEAn extension is not .docx, .doc, .pdf, .pptx or .xlsx, or only one of the two files is a .pptx or a .xlsx
400DOCUMENT_UNREADABLEA .docx source is not a readable Word file: corrupt, or encrypted
401INVALID_API_KEYMissing, malformed, unknown, revoked, or expired key
402USAGE_LIMIT_REACHEDThe organization's monthly upload allowance is exhausted
403API_KEY_ORG_MISMATCHThe key's service account no longer belongs to the key's organization
403TRIAL_COMPARISON_LIMIT_REACHEDThe organization's API trial allowance is used up
413FILE_TOO_LARGEA file exceeds the 100 MB limit
429TRIAL_RATE_LIMITEDThe 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.

ParameterInRequiredDefaultMeaning
comparison_idpathYes—The id from POST /v1/compare
formatqueryNodocx (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.

formatArtifactUse it for
pdfPDF rendering of the redlineShowing a person the redline. Start here
htmlThe interactive redline as one self-contained HTML file. See Embed redlinesAn interactive view beside the PDF: change cards, filters, Previous and Next
docxWord document with real tracked changes (the default)A lawyer to accept, reject and send on in Word
pdf_changed_pages_onlyPDF containing only the pages that changedA quick review of a long agreement
mdThe redline as Markdown, with every change marked inline. See Redline formatA model to read: summaries, questions about the changes, checks against a playbook
jsonThe redline as structured data. See Document JSONCode 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.codeMeaning
DOCUMENT_PROTECTEDA source document is password-protected or permission-restricted
DOCUMENT_UNREADABLEA source document could not be opened: corrupt or malformed
DOCUMENT_CONVERSION_FAILEDA .doc or .pdf source could not be converted to .docx
SOURCE_UPLOAD_FAILEDA source document did not reach storage intact
PDF_RENDER_FAILEDThe redline generated, but its PDF rendering did not
COMPARISON_FAILEDCatch-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

StatusCodeCause
400INVALID_COMPARISON_IDThe id is malformed, or is a mrg_ merge id
400UNSUPPORTED_FORMATA requested format is unknown, or not available for this kind of comparison
403TRIAL_WORD_DOWNLOADS_EXCLUDEDdocx was requested, and the organization's API trial excludes Word downloads
404COMPARISON_NOT_FOUNDNo such comparison, or it belongs to another organization
410CONTENT_DELETEDThe 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:

  • json there is the comparison's older JSON, kept for the integrations built on it. The json in file is the redline's listing.
  • expires_at there keeps its older form, with +00:00 and fractional seconds. Every other time ends in Z.

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:

  1. GET /v1/files/{file_id}?format=json for the redline's file.file_id lists its tracked changes in revisions, each with its index, type, author and the start of its text. The format is pending until the redline exists.
  2. POST /v1/edit with that file_id and one add_comment per change: {"type": "add_comment", "revision": {"index": 2}, "content": "..."}.
  3. 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_at is 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.

StatusCodeCause
400INVALID_COMPARISON_IDThe id is malformed
404COMPARISON_NOT_FOUNDNo such comparison, or it belongs to another organization
409CONFLICTThe comparison is still processing, or was created without zero-day retention
410CONTENT_DELETEDThe 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");
}