Skip to content

Merge

Combine several separately edited revisions of one document into a single draft whose tracked changes are labeled by their source.

View as Markdown

Two endpoints, the same shape as Compare: one submits a base document and its revisions, the other polls for the merged draft.

Base URLhttps://api.versionstory.com
AuthAuthorization: Bearer vs_.... See Authentication

What a merge is for

Three people take the same contract and each edits their own copy. You now have one original and three revisions, and no single document holding everybody's work.

A merge produces that document: one Word draft in which every change from every revision is a tracked change, labeled with the revision it came from. Reviewing it is the ordinary Word workflow — accept and reject, with attribution intact.

This is a different job from comparing. A comparison answers what changed between these two. A merge answers what does everyone's work look like together.

The flow

  1. Upload the base and each revision with POST /v1/files. Each upload returns a file_id.
  2. POST /v1/merge with a JSON body naming the base and its revisions by those ids, each revision with its author. Returns 202 and a merge_id.
  3. GET /v1/merge/{merge_id}?format=pdf until status is ready, then fetch each file.downloads[].url.

A merge takes longer than a comparison, and for a structural reason worth knowing: each revision is first compared against the base, and the merge itself begins only once all of those redlines exist. Expect it to sit in processing for longer than a single comparison would, with the gap widening as you add revisions.

POST /v1/merge

A JSON body (Content-Type: application/json) naming documents already in Version Story.

FieldRequiredMeaning
originalYesThe base document every revision was edited from
revisionsYes, two to tenThe revised copies of the base, in any order
revisions[].authorNoThe name that revision's tracked changes are credited to. Default: the revision's file name
include_cover_sheetNoWhether the merged document opens with the cover sheet explaining how to review it. Default true
include_changes_summaryNoWhether the merged document ends with the summary of each revision's changes. Default true

original and each revision name one document, in one of two forms:

FormNames
{"file_id": "..."}A file you uploaded with POST /v1/files, or an edit's files.clean
{"document_id": "...", "version_id": "..."}A version of one of your Documents. version_id may be "latest"

Upload each document once, then merge:

curl -sS https://api.versionstory.com/v1/files \
  -H "Authorization: Bearer vs_live_..." \
  -F "document=@nda.docx"
# {"file_id": "3f2a9c1e-...", "status": "processing"}

curl -sS https://api.versionstory.com/v1/files \
  -H "Authorization: Bearer vs_live_..." \
  -F "document=@nda_from_counsel.docx"
# {"file_id": "8b41d7a0-...", "status": "processing"}

curl -sS https://api.versionstory.com/v1/files \
  -H "Authorization: Bearer vs_live_..." \
  -F "document=@nda_from_finance.docx"
# {"file_id": "d90c5e12-...", "status": "processing"}

curl -sS https://api.versionstory.com/v1/merge \
  -H "Authorization: Bearer vs_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "original": {"file_id": "3f2a9c1e-..."},
    "revisions": [
      {"file_id": "8b41d7a0-...", "author": "Dana Reyes"},
      {"file_id": "d90c5e12-...", "author": "Sam Patel"}
    ]
  }'

Attributing the changes

Each revision's author labels the tracked changes from that revision. In the merged document, they read Dana Reyes — nda_from_counsel.docx: the name, then the revision's file name, so reviewers can tell whose edit each one is. Without author, a revision's changes are labeled with its file name alone, so pass the names your reviewers should see.

How conflicts show

Each revision is compared against the same base, so the result does not depend on their order. When two revisions change the same words differently, the merged draft deletes the original words once, credited to both revisions, and inserts each revision's replacement beside it under its own author. The reviewer keeps one replacement and rejects the other.

The PDF marks each conflict in the margin. The JSON wraps it in a conflict segment, so code can list the conflicts.

Response — 202 Accepted

{ "merge_id": "mrg_MTp2cy0xOnZ2LTE", "status": "processing" }

merge_id is opaque and stable, and is not interchangeable with a comparison_id — each endpoint rejects the other's ids.

Notes on inputs

  • Every revision must be a revision of the base. Folding together drafts that share no base is a different operation, combine, which the REST API does not offer.
  • A merge takes from two to ten revisions. With a single revision there is nothing to reconcile: the comparison redline already is the combined document, so use POST /v1/compare.
  • Sources may be .docx, .doc, or .pdf, up to 100 MB each. Non-.docx inputs are converted first, so they spend longer in processing. Presentations and spreadsheets cannot be merged.
  • Revisions routinely share a filename — three copies of nda.docx is the normal case. Duplicates are disambiguated automatically.
  • A redline PDF is read as its upload chose: send track_redline_changes with POST /v1/files. See Redline PDFs.

Errors

StatusCodeCause
400INVALID_REQUESTFewer than two or more than ten revisions, a document named in neither form or in both, or an unknown field
400INVALID_FILE_ID, INVALID_DOCUMENT_ID, INVALID_VERSION_IDAn id is malformed
400UNSUPPORTED_FILE_TYPEA document is not a .docx, .doc, or .pdf
400DOCUMENT_UNREADABLEA .docx source is not a readable Word file: corrupt, or encrypted
400TRIAL_BATCH_TOO_LARGEAn API trial organization sent more revisions than its per-minute allowance
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
403ZERO_DAY_RETENTION_UNSUPPORTEDA Document's version was named by an organization with zero-day retention, which keeps no versions
404FILE_NOT_FOUNDNo such file, it belongs to another organization, or it is a redline or a merged draft rather than a document
404DOCUMENT_NOT_FOUND, VERSION_NOT_FOUNDNo such Document or version, or it belongs to another organization
413FILE_TOO_LARGEA file exceeds the 100 MB limit
429TRIAL_RATE_LIMITEDThe trial's per-minute rate is exceeded; wait for Retry-After seconds

Each revision is compared against the base on the way to the merge, so on an API trial a merge uses one comparison of the allowance per revision.

Multipart upload (deprecated)

POST /v1/merge also takes the documents' bytes as multipart/form-data, with the authors in a separate positional list. It is deprecated: new code uploads each document with POST /v1/files and sends the JSON body, where each revision carries its own author. Existing clients keep working.

FieldRequiredMeaning
originalYesThe base document every revision was edited from
revisionsYes, two to ten timesA revised copy of the base — repeat the field per revision
revision_authorsNoName to attribute a revision's tracked changes to — repeat once per revision, in the same order as revisions
include_cover_sheetNoAs in the JSON body. Default true
include_changes_summaryNoAs in the JSON body. Default true
track_redline_changesNotrue turns a redline PDF's insertions and deletions into tracked changes, so the merge reads it as its revised text; see Redline PDFs. Default true
curl -sS https://api.versionstory.com/v1/merge \
  -H "Authorization: Bearer vs_live_..." \
  -F "original=@nda.docx" \
  -F "revisions=@nda_from_counsel.docx" \
  -F "revisions=@nda_from_finance.docx" \
  -F "revision_authors=Dana Reyes" \
  -F "revision_authors=Sam Patel"

Each revision_authors value labels the revision in the same position: the first name goes with the first revisions file, and so on. Send exactly one value per revision; a different count is 400 INVALID_REQUEST. Values are not comma-split: Dana Reyes,Sam Patel is one name. An empty or whitespace-only value keeps that revision's default label, its file name.

The response and errors are the JSON body's, with INVALID_REQUEST also covering a file field that is missing or empty.

GET /v1/merge/{merge_id}

Reports whether the merged document exists yet, and returns signed download URLs once it does. Safe to call as often as you like — it creates nothing and has no side effects.

ParameterInRequiredDefaultMeaning
merge_idpathYes—The id from POST /v1/merge
formatqueryNodocxWhich renderings to return. Name pdf to show the merged document

Formats

As with compare, format is repeatable and also accepts a comma-separated list. A request that names no format gets the Word file.

formatArtifactUse it for
pdfPDF rendering of the merged document, with conflicts drawn in the marginShowing a person the merged document. Start here
docxThe merged Word document, with every revision's changes tracked and labeled (the default)A lawyer to resolve the changes in Word and send on
mdThe merged document as Markdown, with every change marked inline and its revision named. See Redline formatA model to read
jsonThe merged document as structured data, conflicts included. See Document JSONCode that acts on the changes, such as listing the conflicts. Not for display
curl -sS "https://api.versionstory.com/v1/merge/mrg_MTp2cy0xOnZ2LTE?format=pdf,docx" \
  -H "Authorization: Bearer vs_live_..."

Response: ready

file is the merged document as a file, with one download per requested format.

{
  "merge_id": "mrg_MTp2cy0xOnZ2LTE",
  "status": "ready",
  "file": {
    "file_id": "7d1a...",
    "file_name": "Merged nda.docx",
    "status": "ready",
    "downloads": [
      {
        "format": "pdf",
        "url": "https://documents.versionstory.com/...",
        "file_name": "Merged nda.pdf",
        "expires_at": "2026-10-22T22:00:00Z"
      },
      {
        "format": "docx",
        "url": "https://documents.versionstory.com/...",
        "file_name": "Merged nda.docx",
        "expires_at": "2026-10-22T22:00:00Z"
      }
    ]
  }
}

Each url is signed and expires one hour after it was issued, needs no Authorization header, and can be reissued by polling again. status is the merge's, and file.status the merged file's.

Response: still generating

Returned with Retry-After: 1. The PDF and Markdown trail the Word document, so formats that are ready are returned at once and the rest are named in file.pending_formats. status is ready only once every requested format exists.

{
  "merge_id": "mrg_MTp2cy0xOnZ2LTE",
  "status": "processing",
  "file": {
    "file_id": "7d1a...",
    "file_name": "Merged nda.docx",
    "status": "processing",
    "downloads": [],
    "pending_formats": ["pdf", "docx"]
  }
}

Response: failed

Terminal, and reported as 200 OK because the request itself succeeded: it correctly told you the merge is not going to happen. Submit a new merge to retry.

{
  "merge_id": "mrg_MTp2cy0xOnZ2LTE",
  "status": "failed",
  "error": {
    "code": "DOCUMENT_CONVERSION_FAILED",
    "upstream_code": "ONE_OR_MORE_DOCUMENTS_FAILED_TO_CONVERT_TO_DOCX"
  }
}

A merge fails if any of its underlying comparisons fails, since the merge is assembled from them. The error.code values are the same set as Compare, plus MERGE_UNUSABLE when the merge's records can no longer produce a document. A failed merge has no file, except when only its PDF failed: then it reports failed with an error, and file carries the other requested formats with failed_formats: ["pdf"].

Request errors

StatusCodeCause
400INVALID_MERGE_IDThe id is malformed, or is a cmp_ comparison id
400UNSUPPORTED_FORMATA requested format is not docx, pdf, md, or json
403TRIAL_WORD_DOWNLOADS_EXCLUDEDdocx was requested, and the organization's API trial excludes Word downloads
404MERGE_NOT_FOUNDNo such merge, or it belongs to another organization

MERGE_NOT_FOUND covers both "doesn't exist" and "not yours" on purpose, so that ids cannot be probed for existence.

POST /v1/merge/{merge_id}/acknowledge

For an organization with zero-day retention, deletes the merge as soon as you have saved what you need, instead of an hour after it finished: the base and revisions, each revision's redline against the base, and the merged document. No body.

curl -sS -X POST https://api.versionstory.com/v1/merge/mrg_MTp2cy0xOnZ2LTE/acknowledge \
  -H "Authorization: Bearer vs_live_..."
{ "merge_id": "mrg_MTp2cy0xOnZ2LTE", "status": "deletion_queued" }

A merge that is still processing, or that has no zero-day retention, is 409 CONFLICT.

The whole loop, in Python

import time
import requests

BASE = "https://api.versionstory.com"
headers = {"Authorization": "Bearer vs_live_..."}


def upload(path):
    """Store one document and return its file_id."""
    with open(path, "rb") as document:
        response = requests.post(f"{BASE}/v1/files", headers=headers, files={"document": document})
    response.raise_for_status()
    return response.json()["file_id"]


merge = {
    "original": {"file_id": upload("nda.docx")},
    "revisions": [
        {"file_id": upload("nda_from_counsel.docx"), "author": "Dana Reyes"},
        {"file_id": upload("nda_from_finance.docx"), "author": "Sam Patel"},
    ],
}
created = requests.post(f"{BASE}/v1/merge", headers=headers, json=merge)
created.raise_for_status()
merge_id = created.json()["merge_id"]

while True:
    status = requests.get(
        f"{BASE}/v1/merge/{merge_id}", headers=headers, params={"format": "pdf,docx"}
    )
    status.raise_for_status()
    body = status.json()
    if body["status"] == "ready":
        break
    if body["status"] == "failed":
        raise RuntimeError(f"Merge failed: {body['error']['code']}")
    time.sleep(int(status.headers.get("Retry-After", 1)))

for download in body["file"]["downloads"]:
    content = requests.get(download["url"])
    content.raise_for_status()
    with open(download["file_name"], "wb") as out:
        out.write(content.content)
  • Compare: two documents, one redline
  • Files: uploading the documents a merge names
  • Documents: versions a merge can name by document_id and version_id
  • Redline format: how the Markdown marks and attributes each change