Skip to content

Quickstart

Get an API key, compare two documents, and show the PDF redline in your app: with a coding agent in one prompt, or by hand in three steps.

Open as Markdown

With a coding agent

Copy the prompt into Claude Code, Codex or Cursor, in the repository where the redline belongs. The agent does the rest, and asks you only for what it cannot find:

  1. Gets an API key. It looks for one your team already has; otherwise it opens Version Story in your browser, where you sign in or register and click Allow, and it stores the new key the way your codebase stores secrets.
  2. Runs a test comparison on a public pair of NDA drafts, to prove the key and the API work.
  3. Finds where the redline belongs: the screen or the agent tool where your lawyers look at two versions of a document. It confirms the place with you before it builds.
  4. Builds it, PDF first, on a new branch, and shows you the PDF redline working in a running copy of your app.
  5. Offers the next steps: an interactive redline beside the PDF, Word downloads, the changes as text for your own agents, and the changes as data for your code.

An agent can also read the prompt itself at /developers/quickstart.md, so "follow https://www.versionstory.com/developers/quickstart.md" is a prompt of its own.

Read the prompt

Instructions for the coding agent

You are adding Version Story's document comparison API to this codebase. Version Story compares two versions of a legal document (Word or PDF) and returns a redline: the later version with every change marked. Each format has one job, and this prompt uses each only for its job:

  • PDF: what a lawyer sees. Always the first view.
  • HTML: the same redline as an interactive view to embed in an app (step 6).
  • Word: a document with real tracked changes, for the lawyer to accept, reject and send on.
  • Markdown: the redline as text, for a model to read.
  • JSON: the changes as structured data, for code to act on. Never draw a redline from it: it describes the document's structure and changes, not its layout in pages, and the PDF and the HTML already show the redline exactly.

Why the PDF comes first. Explain this to the engineer when you propose the PDF:

  • A redline is a deliverable, not a diff. Engineers new to legal tend to see a redline as a diff. Lawyers know it is a deliverable their clients judge them by.
  • 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 an app draws for itself is not a record: it changes whenever the code does.
  • It looks right everywhere. The same pages in the app, in an agent's chat, in Slack, attached to an email, in a client portal, and on paper. There is no viewer to build.
  • It is what lawyers review, and what clients see. Clean pages with page numbers to cite, and a polished deliverable that carries the firm's name.

Your first deliverable: at the place where lawyers should see what changed between two versions of a document, this codebase calls the Version Story API and shows the PDF redline. That place is one of two kinds (step 3 says how to tell):

  • A screen in an app: the PDF appears on that screen.
  • An AI agent the lawyers work with (a chat assistant, an agent harness, an MCP server of the team's own): the comparison becomes a tool the agent calls, and the PDF appears where the harness shows files to the lawyer, such as an artifact or side panel, or an attachment in the chat.

Build that, and only that, first. When it works, show the engineer and offer the enhancements in step 6.

Done means: in a running copy of the system (local if possible, development otherwise; see Where to run it), the engineer opens the screen or asks the agent for a redline, a live API call compares two test documents, and the PDF redline appears there.

Where to run it

  • Try to get the app running locally first. Follow its README, setup scripts and Docker or compose files, and ask the engineer for anything you cannot find: credentials, seed data, access to internal services.
  • If the app cannot run locally, because it depends on services, data or access that exist only in a shared environment, build and test in the team's development or staging environment instead, through the team's usual deploy process. Ask the engineer before you deploy anything, and tell them which environment you are using.
  • Never production.
  • Steps 1 and 2 need none of this: run them from any shell on the engineer's machine.

Rules

  • Work on a new branch: git checkout -b version-story-poc.
  • The API key is a secret.
    • Store it the way this codebase already stores secrets (a cloud secrets manager, Vault, Doppler, Kubernetes secrets, or an untracked .env), under the name VERSION_STORY_API_KEY.
    • Never commit it, log it, print it in full, or send it to the browser. Call the API from server code only.
  • Use the public test documents below until the engineer confirms their security team has cleared real client documents.
  • Follow this codebase's conventions: its HTTP client, its job or task pattern for slow work, its error handling, its PDF viewer if it has one. Read before you write.
  • Do not change behavior anywhere but the integration point. The existing flow keeps working if the API is unreachable.

1. Get an API key

First look for a Version Story key the team already has, without printing any you find:

  • The environment, and every place this codebase keeps secrets (see Rules): .env files and their examples, the secrets manager or vault it uses, CI and deployment configuration. Search for VERSION_STORY and for values starting vs_live_.
  • Ask the engineer whether they have one, too.

Check a key you found with GET https://api.versionstory.com/v1/reference/errors: 200 means the key is valid, so store it as VERSION_STORY_API_KEY if it is not already and go to step 2; 401 means it was revoked, so get a new one below.

Otherwise get the key yourself, through the engineer's browser. Their only steps are to sign in to Version Story (or register) and click Allow; you create the key and store it.

  • Tell the engineer a browser tab is about to open, and what to do in it.
  • Save the script below outside the repository and run it with python3, passing your own name and the machine's, such as "Claude Code on <hostname>"; it becomes the key's name in the team's Version Story account. It opens the browser, waits for Allow, creates the key, sends the browser to Version Story's "You're signed in" page, and prints JSON whose api_key is the new key. Store api_key as VERSION_STORY_API_KEY in the secrets store you chose (see Rules) without printing it, then delete the script.
  • Where the engineer's browser cannot reach this machine (SSH, a container, a cloud session), put the URL the script prints in your message to the engineer, in full: they may not see your tool output. After Allow their browser lands on a 127.0.0.1 address that fails to load; ask them to paste that address to you, and fetch it from this machine with curl, which finishes the script.
import base64, hashlib, http.server, json, platform, secrets, sys, urllib.error, urllib.parse, urllib.request, webbrowser

APP = "https://app.versionstory.com"
API = "https://api.versionstory.com"
CLIENT_NAME = sys.argv[1] if len(sys.argv) > 1 else f"Coding agent on {platform.node()}"

verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)


def redeem(answer):
    """The new key's JSON for the code the browser brought, or an error to report."""
    if answer.get("state") != state:
        return None, "the browser returned a different state"
    if "code" not in answer:
        return None, answer.get("error", "the browser returned no code")
    request = urllib.request.Request(
        f"{API}/v1/api-keys",
        data=json.dumps({"code": answer["code"], "code_verifier": verifier, "redirect_uri": redirect_uri}).encode(),
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return response.read().decode(), None
    except urllib.error.HTTPError as error:
        return None, error.read().decode()


class Callback(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        parts = urllib.parse.urlsplit(self.path)
        if parts.path != "/callback":
            self.send_response(404)
            self.end_headers()
            return
        answer = dict(urllib.parse.parse_qsl(parts.query))
        self.server.key, self.server.error = redeem(answer)
        result = "granted" if self.server.key else "denied" if answer.get("error") == "access_denied" else "failed"
        self.send_response(302)
        self.send_header("Location", f"{APP}/developer/authorize/complete?result={result}")
        self.end_headers()

    def log_message(self, *args):
        pass


class Listener(http.server.HTTPServer):
    def handle_timeout(self):
        self.timed_out = "nobody clicked Allow within 15 minutes; run the script again"


server = Listener(("127.0.0.1", 0), Callback)
server.key = server.error = server.timed_out = None
server.timeout = 15 * 60  # stop waiting for Allow after 15 minutes
redirect_uri = f"http://127.0.0.1:{server.server_address[1]}/callback"
url = f"{APP}/developer/authorize?" + urllib.parse.urlencode(
    {
        "client_name": CLIENT_NAME,
        "redirect_uri": redirect_uri,
        "code_challenge": challenge,
        "code_challenge_method": "S256",
        "state": state,
    }
)
print(f"Sign in to Version Story and click Allow: {url}", file=sys.stderr, flush=True)
webbrowser.open(url)
while server.key is None and server.error is None:
    server.handle_request()
    server.error = server.error or (None if server.key else server.timed_out)
if server.error:
    sys.exit(f"Sign-in did not finish: {server.error}")
print(server.key)

If the browser shows "Developer access needed", the engineer's organization has not given them developer access yet: ask an owner of their organization, or Version Story at help@versionstory.com, to turn it on, then run the script again.

2. Run a test comparison

Run this before step 3, however you got the key: the check in step 1 proves only the key, and this proves the comparison and the download too. Put the key in this shell's environment as VERSION_STORY_API_KEY, read from where you stored it, without printing it. Then download a test pair. The documents are public, so they need no key:

BASE=https://version-story-evals.s3.us-west-2.amazonaws.com/files/FILE-0189--nda-demo
curl -sO $BASE/v1.docx && curl -sO $BASE/v2.docx

Start a comparison. It answers at once with an id:

curl -s https://api.versionstory.com/v1/compare \
  -H "Authorization: Bearer $VERSION_STORY_API_KEY" \
  -F original=@v1.docx -F modified=@v2.docx -F author="Test"
# {"comparison_id":"cmp_…","status":"processing"}

Get the redline. Always name format=pdf: a call that names no format gets the Word file, which a browser cannot show. The call answers at once; while status is processing, wait the seconds its Retry-After header gives (one) and call again with the same arguments:

curl -s "https://api.versionstory.com/v1/compare/cmp_…?format=pdf" \
  -H "Authorization: Bearer $VERSION_STORY_API_KEY"
# {"comparison_id":"cmp_…","status":"ready","file":{"file_id":"…","status":"ready",
#   "downloads":[{"format":"pdf","url":"https://documents.versionstory.com/…","file_name":"Redline - v1 to v2.pdf","expires_at":"…"}]}}

Download file.downloads[0].url and open the PDF. If this works, the key and the API are fine, and any later problem is in the integration.

3. Find where the redline belongs

First tell which kind of system this is. If lawyers work through an AI agent here (the codebase defines tools for a model, an MCP server, or a chat or agent loop that calls an LLM), the integration point is a new tool that agent calls when a lawyer asks what changed. Otherwise it is a screen. If both exist, ask the engineer which one lawyers use for this work.

Then find the place where a lawyer would want to see what changed between two versions of a document. Good signs:

  • A screen, agent step or tool that holds two versions of the same document: a draft and the counterparty's return, a template and a filled-in agreement, an original and an agent's edited version.
  • A call to another comparison product or library. Version Story replaces that call.
    • Commercial: Litera, Workshare, Draftable, Word's Compare.
    • Open source: Docxodus, Open-XML-PowerTools' WmlComparer, python-redlines, or a text diff library such as diff-match-patch, jsdiff or Python's difflib.
  • An upload or email-ingest path where a new version of an existing document arrives.

Tell the engineer what you found, with file paths, and confirm the place before you build.

4. Build it

  1. Configuration. Read VERSION_STORY_API_KEY from the secrets store, and the base URL https://api.versionstory.com from configuration.
  2. A small server-side client with one function, roughly compare(original, modified, author) -> PDF bytes:
    • POST /v1/compare as multipart/form-data with the two files as original (the earlier version) and modified (the later one). author is optional: the name the tracked changes are attributed to.
    • Then GET /v1/compare/{comparison_id}?format=pdf until status is ready, waiting the Retry-After seconds between calls, with a timeout of a few minutes overall. A typical pair is ready within ten seconds.
    • Download the PDF from file.downloads[].url on the server. Links expire after one hour, so never store a link: store the PDF, or store the comparison_id and ask for a fresh link.
    • Errors arrive as {"error": {"code": …, "message": …, "request_id": …}}. Log the code and request_id (we can trace any request by its id), and surface a plain message to the user. status: failed is final. GET /v1/reference/errors explains every code.
  3. Run it where the work runs. If this app has a background-job pattern, use it: a comparison of long documents can take more than a minute.
  4. Show the PDF at the integration point.
    • A screen: with the app's existing PDF viewer; if there is none, use the simplest one that fits the stack (an <iframe> of a same-origin endpoint that streams the stored PDF, or pdf.js). Show a loading state while the comparison runs.
    • An agent tool: define the tool the way this codebase defines its other tools, taking the two documents in the form the harness already passes files to tools, and the author. Deliver the PDF to the lawyer the way the harness already shows files: an artifact or side panel, an attachment in the chat, or a download link served by this app. If the harness has no way to show a file yet, ask the engineer which to add. Return a short text result to the model (that the redline is ready, and its file name), never the PDF's bytes, which the model cannot read.
  5. A test that runs the client against a recorded response, in this codebase's test style.

Word, PDF and legacy .doc files compare with each other in any combination, so the two versions may arrive in different formats.

5. Show it working

In the local copy of the system, or in development if it could not run locally, compare a test pair, the one most like this team's documents (see Test documents): open the screen, or ask the agent for a redline of the pair. Ask the engineer to look at the redline where it appeared. Then commit on the branch and summarize what you changed, file by file.

6. Offer the next steps

Ask the engineer which of these to build next. Each is a small change to the client from step 4: ask for more formats in the same GET, for example ?format=pdf,html,docx.

  1. A redline your agents can read. format=md is the later version as Markdown, one paragraph per line, with every change inline: <ins>, <del>, and <moved-from> and <moved-to> for moved text. A comment at the top explains the tags, and a list after the document gives every change with its author and date. Give it to an agent as context when it summarizes changes, checks them against a playbook, or answers "what changed in Section 7?" For an agent tool, this is the text to return to the model with the PDF.
  2. Download buttons. format=docx is a Word document with real tracked changes, which a lawyer can accept, reject and send on. format=pdf_changed_pages_only is a PDF of the changed pages only, for a quick review of a long agreement. Add the buttons beside the PDF, or for an agent tool, deliver them as more files the same way as the PDF.
  3. An interactive redline in the app. Keep the PDF as the default view, since it is exactly what the lawyer would print or send. Add an interactive view beside it with the HTML redline, the same redline laid out in pages with its own Revisions panel: change cards, filters by type, and Previous and Next. Read https://www.versionstory.com/developers/rest/embed before building it.
    • On the server, call GET /v1/compare/{comparison_id}?format=html with the key, and return the signed link in file.downloads[].url to the page. Never send the key to the browser, and never store the link: it expires after an hour, so fetch a fresh one on view.
    • On the page, frame it: <iframe sandbox="allow-scripts" src="{url}#embed_origin=<this app's origin>">. The options go in the fragment, never the query, which would break the link's signature. Never add allow-same-origin, and never put the HTML into the app's own DOM.
    • Driving it from the app (optional): the page can move to the next change or to a paragraph and hear where the lawyer is, over postMessage with vs:* messages; the embed page lists them and how to turn off parts of the Revisions panel the app draws itself.
    • Do not build a viewer from format=json instead. The JSON describes the document's structure and changes, not its layout in pages, so a viewer drawn from it re-implements Word's layout and still shows less than the HTML does.
  4. Structured data for your app's logic. format=json lets deterministic code read the changes without parsing a document: count insertions and deletions, list the paragraphs that changed, flag a change in a section your app cares about, or link a change to your own records (its paragraph id is what the HTML redline scrolls to). It is for what the code decides, never for what the lawyer sees.

If this organization has zero-day retention turned on, call POST /v1/compare/{comparison_id}/acknowledge once every format you need is stored: Version Story then deletes the documents and the redline immediately.


Test documents

Each set is a pair of versions of one agreement, v1.docx (the original) and v2.docx (the later version), public so anyone can download them:

SetWhat it is
FILE-0189--nda-demoA four-page NDA, then the version with the parties, date and signature blocks filled in, a paragraph moved, and word-level edits throughout. Start here.
FILE-0190--consulting-demoA consulting agreement, then the consultant's heavy mark-up: parties, services, payment, IP ownership, confidentiality and term.
FILE-0191--table-demoA licence agreement's special-terms table as a blank template, then filled in: rows renumbered and removed, and long clauses inside the table's cells.

API reference

CallWhat it does
POST /v1/api-keysRedeems the browser sign-in's code for a new key (step 1); the one call that takes no key.
POST /v1/compareStarts a comparison of original and modified; returns comparison_id.
GET /v1/compare/{comparison_id}?format=…The comparison's status and, once ready, download links in file.downloads: pdf (to show; always name it, since the default is docx), html (the interactive redline to frame in a sandboxed iframe, step 6), docx (to edit in Word), md (for a model), json (for code, never for display). pdf_changed_pages_only is in the top-level downloads.
POST /v1/compare/{comparison_id}/acknowledgeWith zero-day retention, deletes the content once you have saved it.
GET /v1/reference/{topic}Reference topics: errors, redline (the change markup).

Help

If the API does not behave as this file describes, stop and tell the engineer, with the error's code and request_id. Version Story's team will help directly at help@versionstory.com.

By hand

Three steps: get a key, compare two documents, and show the redline as a PDF.

1. Get an API key

Sign up, open the Developer page from the sidebar, and create a key under API keys. The key is shown once, so store it straight away: in your secrets manager or an untracked .env, never in source control or in browser code. Authentication covers keys in full, including how a coding agent can get one for you with a single click.

A new organization starts on an API trial, which is plenty for a first integration.

2. Compare two documents

Version Story publishes a test pair, a four-page NDA and its marked-up return. It needs no key to download:

BASE=https://version-story-evals.s3.us-west-2.amazonaws.com/files/FILE-0189--nda-demo
curl -sO $BASE/v1.docx && curl -sO $BASE/v2.docx

Submit the two versions. original is the earlier one and modified the later. The call answers at once with 202 Accepted and an id; nothing is generated yet.

curl -sS https://api.versionstory.com/v1/compare \
  -H "Authorization: Bearer $VERSION_STORY_API_KEY" \
  -F "original=@v1.docx" \
  -F "modified=@v2.docx" \
  -F "author=Dana Reyes"
{ "comparison_id": "cmp_MTp2cy0xOnZtLTE", "status": "processing", "file": { "file_id": "3f2c...", "status": "processing" } }

author is optional: the name Word shows on the tracked changes.

Ask for the PDF until it is ready. Name format=pdf. A request that names no format gets the Word file, which a browser cannot show. While the redline is being made, the response carries Retry-After: 1, so wait a second between calls.

curl -sS "https://api.versionstory.com/v1/compare/cmp_MTp2cy0xOnZtLTE?format=pdf" \
  -H "Authorization: Bearer $VERSION_STORY_API_KEY"
{
  "comparison_id": "cmp_MTp2cy0xOnZtLTE",
  "status": "ready",
  "file": {
    "file_id": "3f2c...",
    "file_name": "Redline - v1 to v2.docx",
    "status": "ready",
    "downloads": [
      {
        "format": "pdf",
        "url": "https://documents.versionstory.com/...",
        "file_name": "Redline - v1 to v2.pdf",
        "expires_at": "2026-10-22T04:37:34Z"
      }
    ]
  }
}

A typical pair is ready in under ten seconds. Download it from file.downloads[0].url. The link is signed, so it needs no key, and it works for one hour. Asking again gives a fresh one.

curl -sS --fail -o redline.pdf "<file.downloads[0].url>"

Open redline.pdf. That is the redline your lawyers will see.

3. Show the redline in your app

Start with the PDF. It is the record a lawyer keeps and sends: the changes between two drafts exactly as they stood, the same pages in your app, in an email, in Slack and on paper. Why the PDF says more.

Since links expire after an hour, store the comparison_id (or the PDF itself), never the link. When someone opens the redline, your server asks for a fresh link and streams the PDF to the page, so the key stays on your server and the page loads the PDF from your own origin.

On your server, one route that streams the PDF. In Node (18 or later, no dependencies):

// GET /redlines/:comparisonId.pdf
async function streamRedline(comparisonId, response) {
  const api = `https://api.versionstory.com/v1/compare/${encodeURIComponent(comparisonId)}?format=pdf`;
  const headers = { Authorization: `Bearer ${process.env.VERSION_STORY_API_KEY}` };

  for (let attempt = 0; attempt < 120; attempt++) {
    const status = await fetch(api, { headers });
    const body = await status.json();
    if (!status.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);
    if (body.status === "failed") throw new Error(`Comparison failed: ${body.error?.code}`);
    if (body.status === "ready") {
      const pdf = await fetch(body.file.downloads[0].url);
      response.writeHead(200, { "Content-Type": "application/pdf" });
      return response.end(Buffer.from(await pdf.arrayBuffer()));
    }
    await new Promise((resolve) => setTimeout(resolve, Number(status.headers.get("Retry-After") ?? 1) * 1000));
  }
  throw new Error("The redline was not ready in time");
}

The same route in Python, with Flask:

import os, time, requests
from flask import Flask, Response

app = Flask(__name__)
API = "https://api.versionstory.com"
HEADERS = {"Authorization": f"Bearer {os.environ['VERSION_STORY_API_KEY']}"}


@app.get("/redlines/<comparison_id>.pdf")
def redline_pdf(comparison_id):
    for _ in range(120):
        status = requests.get(f"{API}/v1/compare/{comparison_id}", headers=HEADERS, params={"format": "pdf"})
        status.raise_for_status()
        body = status.json()
        if body["status"] == "failed":
            raise RuntimeError(f"Comparison failed: {body['error']['code']}")
        if body["status"] == "ready":
            pdf = requests.get(body["file"]["downloads"][0]["url"])
            pdf.raise_for_status()
            return Response(pdf.content, mimetype="application/pdf")
        time.sleep(int(status.headers.get("Retry-After", 1)))
    raise TimeoutError("The redline was not ready in time")

On the page, frame it. The browser's own PDF viewer shows the redline with its pages, zoom and print:

<iframe src="/redlines/cmp_MTp2cy0xOnZtLTE.pdf" title="Redline" style="width: 100%; height: 80vh; border: 0"></iframe>

In React, the same frame with a loading state while the comparison finishes:

import { useState } from "react";

function Redline({ comparisonId }) {
  const [loaded, setLoaded] = useState(false);
  return (
    <>
      {!loaded && <p>Preparing the redline…</p>}
      <iframe
        src={`/redlines/${comparisonId}.pdf`}
        title="Redline"
        onLoad={() => setLoaded(true)}
        style={{ width: "100%", height: "80vh", border: 0 }}
      />
    </>
  );
}

If your app already has a PDF viewer, such as pdf.js, point it at the same route. Comparing two long documents can take more than a minute, so if your app has a background-job pattern, run the comparison there and show the PDF when it is done.

For an AI agent rather than a screen, the comparison becomes a tool the agent calls, and the PDF goes wherever your harness shows files to the lawyer: an artifact or side panel, or an attachment in the chat.

Next

With the PDF working, ask for more in the same call: format is repeatable and takes a comma-separated list, so ?format=pdf,docx returns both.

  • An interactive view. Embed the HTML redline beside the PDF: the same redline in pages, with change cards, filters, and Previous and Next. See Embed redlines.
  • A Word file with tracked changes, for a lawyer to accept, reject and send on: format=docx.
  • The pages that changed, for a quick review of a long agreement: format=pdf_changed_pages_only.
  • The changes for a model to read: format=md. See Redline format.
  • The changes for your code to act on, such as counting them or flagging a clause: format=json. It describes the document's structure and changes, not its pages, so it is never the view a lawyer sees. See Document JSON.

To fold several people's edits to one document into a single draft, use Merge. Compare has every field, status and error, and Errors says what to do about each code.