# Selective compare

> Compare a range of paragraphs in one document with a range in another, and get the result at once as JSON, Markdown and a PDF.

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

`POST /v1/compare/selective` compares a range of paragraphs in one document with a range in
another, or two ranges of the same document, such as one clause in two drafts. It answers in the
same response with the comparison as JSON and Markdown, and stores it as a new PDF file. It makes
no Word redline; to redline whole documents, use [Compare](/developers/rest/compare).

| | |
| --- | --- |
| Base URL | `https://api.versionstory.com` |
| Auth | `Authorization: Bearer vs_...`. See [Authentication](/developers/authentication) |

## The flow

1. **Upload** each document with [`POST /v1/files`](/developers/rest/files#post-v1files), or use
   the `file_id` of a file you already have: an upload, an edit's `files.clean`, or a comparison's
   redline.
2. **Find the ranges.** Read each file's [listing](/developers/rest/edit#the-listing) with
   [`GET /v1/files/{file_id}?format=md`](/developers/rest/files#get-v1filesfileid) (or `json`). It
   prints every paragraph's `index` and `id`; note the first and last paragraph of each range.
3. **Compare** with `POST /v1/compare/selective`. The response carries the comparison and its PDF
   file, and nothing is left to poll.

## POST /v1/compare/selective

`application/json`, with an `original` and a `modified` range. Each names its file and runs `from`
its first paragraph `through` its last, as an edit's
[range of paragraphs](/developers/rest/edit#locating-paragraphs) does.

| Field | Required | Meaning |
| --- | --- | --- |
| `file_id` | Yes | The file holding the range. Both sides may name the same file |
| `from` | Yes | The range's first paragraph, as a paragraph locator |
| `through` | Yes | The range's last paragraph, as a paragraph locator: `from` itself or a paragraph after it |

### Naming a paragraph

`from` and `through` name a paragraph as an edit does, with the same
[paragraph locator](/developers/rest/edit-operations#paragraph-locators), so a locator copied from
the listing is sent as it is printed:

| Locator | Names |
| --- | --- |
| `{"index": 12}` | Body paragraph 12 |
| `{"id": "8532bf246ab64e8788c26d27cfd105b4"}` | The paragraph with this id |
| `{"index": 12, "id": "8532bf246ab64e8788c26d27cfd105b4"}` | Both, which must name the same paragraph |

A range is in the body: an index counts the body's paragraphs from 0 in document order, paragraphs
in tables included, as the listing numbers them. A locator with a header, footer or note `part`, or a
`ref`, is refused. The two ends of a side can be named differently, one by index and the other by id.

```bash
curl -sS https://api.versionstory.com/v1/compare/selective \
  -H "Authorization: Bearer vs_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "original": {"file_id": "3f2a9c1e-...", "from": {"index": 12}, "through": {"index": 18}},
    "modified": {"file_id": "8c41d7b2-...", "from": {"id": "8532bf246ab64e8788c26d27cfd105b4"}, "through": {"index": 19}}
  }'
```

Ranges are inclusive. Each paragraph is read with its tracked changes accepted, and Word's list
numbering is compared apart from the text. Paragraphs are lined up first, with identical paragraphs
as anchors and the rest paired by how similar their words are, and then each pair is compared word
by word.

## Response: 200 OK

```json
{
  "comparison": {
    "schema_version": 1,
    "original": { "file_id": "3f2a9c1e-...", "file_name": "v1 NDA.docx", "paragraph_count": 7 },
    "modified": { "file_id": "8c41d7b2-...", "file_name": "v2 NDA.docx", "paragraph_count": 8 },
    "summary": { "unchanged": 5, "modified": 2, "inserted": 1, "deleted": 0 },
    "paragraphs": [
      {
        "status": "modified",
        "original_id": "...",
        "modified_id": "...",
        "original_number": "2.",
        "modified_number": "2.",
        "content": [
          { "text": "through no fault" },
          { "type": "insertion", "text": ", action, or omission" },
          { "text": " of the Recipient;" }
        ]
      }
    ]
  },
  "markdown": "...",
  "file": {
    "file_id": "b4056e05-...",
    "file_name": "Selective Compare - v1 NDA to v2 NDA.pdf",
    "status": "ready",
    "downloads": [
      {
        "format": "pdf",
        "url": "https://documents.versionstory.com/...",
        "file_name": "Selective Compare - v1 NDA to v2 NDA.pdf",
        "expires_at": "2026-11-03T19:30:00Z"
      }
    ]
  },
  "message": "The comparison is complete. Tell the user what changed between the two ranges ..."
}
```

| Field | Meaning |
| --- | --- |
| `comparison.original`, `comparison.modified` | Each side's file and how many paragraphs its range holds |
| `comparison.summary` | How many lined-up paragraphs have each `status` |
| `comparison.paragraphs` | The lined-up paragraphs in reading order. `status` is `unchanged`, `modified`, `inserted` (with only `modified_id`) or `deleted` (with only `original_id`). A `content` segment without `type` is unchanged text: skip the insertions to read the original, and the deletions to read the modified |
| `markdown` | The same result with changes marked by `<ins>` and `<del>`, each paragraph opened by `<p original-unid="..." modified-unid="..."/>` |
| `file` | A new [file](/developers/rest/files) holding the same result as a PDF. See [The PDF file](#the-pdf-file) |
| `message` | What an agent should do next with the result, as in other responses |

### The PDF file

The PDF is the one the Selective Compare panel exports in the Version Story app: inserted text blue
and underlined, deleted text red and struck through, each paragraph on its own line after its list
number. It is written before the response is sent, so `file` is always `ready`. Its download URL is
valid for one hour; [`GET /v1/files/{file_id}`](/developers/rest/files#get-v1filesfileid) with its
`file_id` mints a fresh one. It is served as `pdf` only, and it cannot be compared, edited or read
as a listing. For an organization with
[zero-day retention](/developers/reference/data-handling#retention), it is deleted an hour after it
is made, or as soon as you call
[`POST /v1/files/{file_id}/acknowledge`](/developers/rest/files#post-v1filesfileidacknowledge).

Each request makes a new file, so send it once and keep the `file_id`. It counts toward your usage
as a document with the words of the compared text; downloading its PDF, then or later, does not
count again.

## Errors

| Status | Code | Cause |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | A missing `from` or `through`, a locator that names no paragraph or names one outside the body, an index and id that name different paragraphs, a range that runs backwards, or a file with no listing to compare, such as another selective comparison's PDF |
| 400 | `PARAGRAPH_NOT_FOUND` | A paragraph index or id is not in its file |
| 400 | `RANGE_TOO_LARGE` | The ranges are too large to compare in one request: more than 40,000 pairs of paragraphs between identical ones, or a comparison of more than 300,000 characters. Compare smaller ranges |
| 400 | `UNSUPPORTED_FILE_TYPE` | A workbook or presentation, which has no paragraphs |
| 400 | `INVALID_FILE_ID` | A `file_id` is malformed |
| 401 | `INVALID_API_KEY` | Missing, malformed, unknown, revoked, or expired key |
| 404 | `FILE_NOT_FOUND` | No such file, or it belongs to another organization |
| 409 | `CONFLICT` | A file is still being prepared (retry after `Retry-After`), or its preparation failed (upload it again) |
| 410 | `CONTENT_DELETED` | A file's content was deleted under zero-day retention |

## Related

- [Compare](/developers/rest/compare): a Word redline of two whole documents
- [Edit operations](/developers/rest/edit-operations#paragraph-locators): every form of a paragraph locator
- [Files](/developers/rest/files): upload documents and download the comparison's PDF again
- [Data handling](/developers/reference/data-handling): what is kept, and for how long
