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:
- 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.
- Runs a test comparison on a public pair of NDA drafts, to prove the key and the API work.
- 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.
- Builds it, PDF first, on a new branch, and shows you the PDF redline working in a running copy of your app.
- 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 nameVERSION_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.
- Store it the way this codebase already stores secrets (a cloud secrets manager, Vault,
Doppler, Kubernetes secrets, or an untracked
- 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):
.envfiles and their examples, the secrets manager or vault it uses, CI and deployment configuration. Search forVERSION_STORYand for values startingvs_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 whoseapi_keyis the new key. Storeapi_keyasVERSION_STORY_API_KEYin 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.1address 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.docxStart 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
- Configuration. Read
VERSION_STORY_API_KEYfrom the secrets store, and the base URLhttps://api.versionstory.comfrom configuration. - A small server-side client with one function, roughly
compare(original, modified, author) -> PDF bytes:POST /v1/compareasmultipart/form-datawith the two files asoriginal(the earlier version) andmodified(the later one).authoris optional: the name the tracked changes are attributed to.- Then
GET /v1/compare/{comparison_id}?format=pdfuntilstatusisready, waiting theRetry-Afterseconds between calls, with a timeout of a few minutes overall. A typical pair is ready within ten seconds. - Download the PDF from
file.downloads[].urlon the server. Links expire after one hour, so never store a link: store the PDF, or store thecomparison_idand ask for a fresh link. - Errors arrive as
{"error": {"code": …, "message": …, "request_id": …}}. Log thecodeandrequest_id(we can trace any request by its id), and surface a plain message to the user.status: failedis final.GET /v1/reference/errorsexplains every code.
- 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.
- 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.
- A screen: with the app's existing PDF viewer; if there is none, use the simplest one that
fits the stack (an
- 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.
- A redline your agents can read.
format=mdis 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. - Download buttons.
format=docxis a Word document with real tracked changes, which a lawyer can accept, reject and send on.format=pdf_changed_pages_onlyis 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. - 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=htmlwith the key, and return the signed link infile.downloads[].urlto 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 addallow-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
postMessagewithvs:*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=jsoninstead. 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.
- On the server, call
- Structured data for your app's logic.
format=jsonlets 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 paragraphidis 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:
| Set | What it is |
|---|---|
FILE-0189--nda-demo | A 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-demo | A consulting agreement, then the consultant's heavy mark-up: parties, services, payment, IP ownership, confidentiality and term. |
FILE-0191--table-demo | A 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. |
- One file:
https://version-story-evals.s3.us-west-2.amazonaws.com/files/<set>/v1.docx - Every set, as JSON (each file's URL and sha256): https://version-story-evals.s3.us-west-2.amazonaws.com/index.json
API reference
| Call | What it does |
|---|---|
POST /v1/api-keys | Redeems the browser sign-in's code for a new key (step 1); the one call that takes no key. |
POST /v1/compare | Starts 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}/acknowledge | With zero-day retention, deletes the content once you have saved it. |
GET /v1/reference/{topic} | Reference topics: errors, redline (the change markup). |
- Every request sends
Authorization: Bearer <VERSION_STORY_API_KEY>. - The full OpenAPI description, which needs no key:
https://api.versionstory.com/openapi.json. - Documentation: https://www.versionstory.com/developers/rest/compare, and every page as Markdown from https://www.versionstory.com/llms.txt.
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.docxSubmit 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.