OpenAPI 3.1.0, version 1.0.0
API reference
The routes behind Clause Desk, a demonstration for the knowledge team of a fictional law firm. Seeded contracts are public. An upload is private to the visitor's signed cd_anon cookie and is deleted after 24 hours; another visitor gets 404 for it, exactly as for a document that does not exist. Every error has the same JSON shape, and every response carries an X-Request-Id that matches the server log. There are no accounts and no keys.
openapi.jsonhttps://clauses.phoenixcodes.org
GET/api/documents
List documents
The seeded contracts, plus the caller's own uploads when the request carries a valid cd_anon cookie. The answer depends on the cookie, so it is never cached.
Example
curl 'https://clauses.phoenixcodes.org/api/documents'Responses
- 200The documents the caller may read.application/json · object
- 500Something failed on the server (
internal). Quote the request id.application/json · Error
GET/api/documents/{id}/pdf
Get a document's PDF
The PDF bytes, inline. A seeded contract is cached for an hour; an upload is private, no-store and only its owner can read it.
Parameters
idstringpathrequired- A document id: a slug for a seeded contract (
mutual-nda) oru-and a short random id for an upload.
Example
curl 'https://clauses.phoenixcodes.org/api/documents/msa/pdf'Responses
- 200The PDF.application/pdf · string (application/pdf)
- 400The request is not valid.
messagesays which field and why.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 500Something failed on the server (
internal). Quote the request id.application/json · Error
GET/api/documents/{id}/geometry/{page}
Get a page's text and line boxes
The text of one page and the box of every line, in PDF points with the origin at the top-left. A character range from a citation becomes highlight rectangles with these. Offsets are UTF-16 offsets into text.
Parameters
idstringpathrequired- A document id: a slug for a seeded contract (
mutual-nda) oru-and a short random id for an upload. pageintegerpathrequired- 1 to 20001-based page number.
Example
curl 'https://clauses.phoenixcodes.org/api/documents/msa/geometry/%E2%80%A6'Responses
- 200The page.application/json · PageGeometry
- 400The request is not valid.
messagesays which field and why.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 500Something failed on the server (
internal). Quote the request id.application/json · Error
POST/api/ask
Ask a question about a document
Retrieves clauses from the one document named in the request, and streams an answer that cites them as [p.9 §11.2]. The reply is a server-sent stream in the AI SDK UI message format. Below the confidence threshold no model is called and the stream carries the single sentence Not addressed in this document. with the closest clauses in data-meta.related.
The stream's parts: start; data-meta (confidence first, and again at the end with the model, tokens, cost and timings); data-passages (every passage the model was shown, numbered, with the character ranges a viewer highlights); text-start, text-delta and text-end for the answer; error; finish. A failure after the first token ends the stream with an error part and never splices a second model's answer onto the first.
Each client gets 15 questions per 10 minutes, and the whole demo has a daily token budget that resets at 00:00 UTC.
Request body (JSON)
documentIdstringrequired- The one document to answer from.
questionstringrequired- 3 to 500 charactersThe question. Whitespace at either end is trimmed before the length is checked.
sessionIdstring (uuid)- Continue a session. Omit to start one; the id comes back in
data-meta.
Example
curl -N -X POST 'https://clauses.phoenixcodes.org/api/ask' \
-H 'Content-Type: application/json' \
-d '{"documentId":"msa","question":"What is the cap on each party'\''s liability in a contract year?"}'Responses
- 200The answer, streamed.text/event-stream · string
- 400The request is not valid.
messagesays which field and why.application/json · Error - 403A browser sent the request from another site (
forbidden). Only this site's own pages may spend the model budget; curl and servers, which send noOrigin, are not affected.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 429Too many requests from this client (
rate_limited), or too many live uploads (too_many_uploads). WaitretryAfterSeconds.application/json · Error - 503A dependency (search, a model or the database) is unavailable (
upstream_unavailable), or the daily token budget is used up (budget_exhausted, withresetsAt).application/json · Error
POST/api/upload
Upload a PDF
Takes a text PDF of at most 10 MB and 80 pages. The platform caps a request body at 4.5 MB, so the file is sent in parts of at most 4 MB (up to 3), each as the raw body of one request, tied together by x-upload-id. Every part but the last answers 202. The last answers 200 with a stream of newline-delimited JSON progress events, ending in done with the new document, or error.
The upload is private to the caller's cd_anon cookie (set on this response if the caller has none) and deleted after 24 hours. Scanned PDFs, password-protected PDFs and damaged files are refused with a reason; there is no OCR.
Parameters
x-upload-partstringheader- Which part this is, as
2/3. Omit for a file sent in one request. x-upload-idstringheader- Ties the parts together. Required when there is more than one part.
x-upload-sizeintegerheader- 1 to 10485760The whole file's size in bytes. Required when there is more than one part.
x-upload-namestringheader- The file's name, used for the title when the PDF has none.
Request body
application/pdf: The raw bytes of one part, at most 4,000,000.
Example
curl -N -X POST 'https://clauses.phoenixcodes.org/api/upload' \
-H 'x-upload-part: 1/3' \
-H 'x-upload-id: 7f3a9c1e-5b2d' \
-H 'x-upload-size: 9200000' \
-H 'Content-Type: application/pdf' \
--data-binary @contract.pdfResponses
- 200Progress events, one JSON object per line, ending in
doneorerror.application/x-ndjson · UploadEvent - 202A part before the last was stored.application/json · object
- 400The request is not valid.
messagesays which field and why.application/json · Error - 403A browser sent the request from another site (
forbidden). Only this site's own pages may spend the model budget; curl and servers, which send noOrigin, are not affected.application/json · Error - 413A part or the whole file is over the limit (
payload_too_large).application/json · Error - 415The body is not sent as
application/pdf(unsupported_media_type).application/json · Error - 422The first bytes are not a PDF's (
not_a_pdf).application/json · Error - 429Too many requests from this client (
rate_limited), or too many live uploads (too_many_uploads). WaitretryAfterSeconds.application/json · Error
GET/api/compare
Compare two documents
Aligns the clauses of two documents by text, heading and number, so a renumbered or dropped article still pairs, and returns the word-level diff of each pair, the list of changes and any saved one-line summaries. The result is rebuilt on every request; only the model-written summaries are stored. Both documents must be readable by the caller.
Parameters
leftstringqueryrequired- The earlier document's id.
rightstringqueryrequired- The later document's id. Must differ from
left.
Example
curl 'https://clauses.phoenixcodes.org/api/compare?left=mutual-nda&right=mutual-nda-v2'Responses
- 200The comparison.application/json · Comparison
- 400The request is not valid.
messagesays which field and why.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 503A dependency (search, a model or the database) is unavailable (
upstream_unavailable), or the daily token budget is used up (budget_exhausted, withresetsAt).application/json · Error
POST/api/compare/gloss
Write the one-line summaries of a comparison
Has a model write one sentence per change, once, and saves them for the pair. A sentence that mentions a number the change does not contain is dropped. Nothing is charged to the rate limit or the budget when the summaries already exist or no model is configured. 8 summary requests per 10 minutes per client.
Request body (JSON)
leftstringrequired- The earlier document's id.
rightstringrequired- The later document's id.
Example
curl -X POST 'https://clauses.phoenixcodes.org/api/compare/gloss' \
-H 'Content-Type: application/json' \
-d '{"left":"mutual-nda","right":"mutual-nda-v2"}'Responses
- 200The comparison with its summaries, and how many were newly written.application/json · Comparison
- 400The request is not valid.
messagesays which field and why.application/json · Error - 403A browser sent the request from another site (
forbidden). Only this site's own pages may spend the model budget; curl and servers, which send noOrigin, are not affected.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 429Too many requests from this client (
rate_limited), or too many live uploads (too_many_uploads). WaitretryAfterSeconds.application/json · Error - 503A dependency (search, a model or the database) is unavailable (
upstream_unavailable), or the daily token budget is used up (budget_exhausted, withresetsAt).application/json · Error
GET/api/sessions/{id}/export
Export a question session
The questions and answers of one session with every citation kept: Markdown with GitHub footnotes, or an A4 PDF typeset in the site's type and colours with the citations as footnotes. Only the visitor who asked the questions can export them; anyone else gets 404. 10 exports per 10 minutes per client.
Parameters
idstring (uuid)pathrequired- The session id, from
data-meta.sessionIdin an ask stream. format"pdf" | "md"query- default "pdf"The file format.
Example
curl 'https://clauses.phoenixcodes.org/api/sessions/%E2%80%A6/export'Responses
- 200The export, as an attachment. Never cached.application/pdf · string (application/pdf)text/markdown · string
- 400The request is not valid.
messagesays which field and why.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 429Too many requests from this client (
rate_limited), or too many live uploads (too_many_uploads). WaitretryAfterSeconds.application/json · Error
GET/api/eval/runs
List eval runs
Stored scorecards from pnpm eval, newest first, without the per-question results. A run with retrievalOnly: true measured retrieval and the confidence gate but had no answer model, so its answer figures are null rather than zero.
Parameters
limitintegerquery- 1 to 50 · default 10How many runs to return.
Example
curl 'https://clauses.phoenixcodes.org/api/eval/runs'Responses
- 200The runs.application/json · object
- 400The request is not valid.
messagesays which field and why.application/json · Error - 503A dependency (search, a model or the database) is unavailable (
upstream_unavailable), or the daily token budget is used up (budget_exhausted, withresetsAt).application/json · Error
GET/api/eval/runs/{id}
Get one eval run
One stored run with the result of every question.
Parameters
idstring (uuid)pathrequired- The run id.
Example
curl 'https://clauses.phoenixcodes.org/api/eval/runs/%E2%80%A6'Responses
- 200The run and its questions.application/json · EvalRunDetail
- 400The request is not valid.
messagesays which field and why.application/json · Error - 404There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.application/json · Error
- 503A dependency (search, a model or the database) is unavailable (
upstream_unavailable), or the daily token budget is used up (budget_exhausted, withresetsAt).application/json · Error
GET/api/cron/purge
Delete expired uploads
Called once a day by the platform's scheduler, which sends Authorization: Bearer <CRON_SECRET>. Deletes uploads past their 24 hours and abandoned upload parts. Without the secret the answer is 401. Reads of an expired upload are refused whether or not this has run; the sweep reclaims space.
Example
curl 'https://clauses.phoenixcodes.org/api/cron/purge' \
-H 'Authorization: Bearer $CRON_SECRET'Responses
- 200How many uploads were removed.application/json · object
- 401The bearer secret is missing or wrong.application/json · Error
- 500Something failed on the server (
internal). Quote the request id.application/json · Error
Schemas
The shapes the routes above send and return. AskMeta and Passage travel inside the ask stream as the data-meta and data-passages parts.
AskMeta
The data-meta part of an ask stream. Sent first (confidence, before any model call) and again at the end with the outcome.
sessionIdstring (uuid)requiredquestionIdstring (uuid)requiredanswerIdstring (uuid)requireddocumentIdstringrequiredconfidencenumberrequired- 0 to 1
thresholdnumberrequired- 0 to 1
gateDeclinedbooleanrequired- The confidence gate stopped the request before any model was called.
declinedbooleanrequired- The reply is
Not addressed in this document.(gate or model). relatedinteger[]- With a decline: the numbers (
Passage.n) of the closest clauses. provider"anthropic" | "workers-ai"modelstringpromptTokensintegercompletionTokensintegercostUsdnumber- 0 on Workers AI.
ttftMsinteger- Milliseconds to the first streamed token.
latencyMsinteger
Passage
The data-passages part: one retrieved clause chunk, numbered in rank order.
nintegerrequired- at least 1
chunkIdstringrequireddocumentIdstringrequiredlabelstringrequired- The reference the answer writes between brackets, e.g.
p.9 §11.2. pageStartintegerrequiredpageEndintegerrequiredclauseIdstring | nullrequired- Primary clause number of the chunk.
clauseIdsstring[]requiredheadingstringrequiredbreadcrumbstringrequiredrangesTextRange[]required- The character ranges of the clause's own text, per page.
excerptstringrequired
UploadEvent
One line of the upload's progress stream. stage runs received, reading, indexing, saving, then done or error; progress only goes up.
stage"received" | "reading" | "indexing" | "saving" | "done" | "error"requiredprogressnumber- 0 to 1
doneintegerindexing: chunks embedded so far.totalintegerindexing: chunks to embed.documentDocumentSummarydone: the new document.errorobjecterror: why it stopped.error.code"invalid_request" | "invalid_id" | "not_found" | "unauthorized" | "forbidden" | "rate_limited" | "budget_exhausted" | "upstream_unavailable" | "payload_too_large" | "unsupported_media_type" | "not_a_pdf" | "scanned_pdf" | "encrypted_pdf" | "too_many_pages" | "too_many_uploads" | "internal"requirederror.messagestringrequired- Safe to show to a person.
error.requestIdstringerror.retryAfterSecondsintegerrate_limitedandbudget_exhausted: seconds until a request can succeed.error.resetsAtstring (date-time)budget_exhausted: when the daily budget resets (00:00 UTC).
DocumentSummary
idstringrequiredtitlestringrequireddocTypestringrequiredpartiesstring[]requiredsummarystringrequiredversionGroupstring | nullrequired- Documents sharing a group are versions of one another.
versionintegerrequiredpageCountintegerrequiredkind"seed" | "upload"requiredseedcontracts are public;uploadones are private and expire.expiresAtstring (date-time) | nullrequired- When an upload is deleted; null for seeded contracts.
Comparison
leftDocumentSummaryrequiredrightDocumentSummaryrequiredrowsCompareRow[]requiredchangesClauseChange[]requiredstatsCompareStatsrequiredglossobjectrequiredgloss.state"ready" | "pending" | "unavailable"requiredready: every change has a summary.pending: a model can write them (POST /api/compare/gloss).unavailable: no model is configured.gloss.modelstring | nullrequired- The model that wrote the summaries.
writteninteger- POST only: summaries newly written by this call.
CompareRow
One aligned clause pair, or an added or removed clause.
keystringrequired- Stable anchor within one comparison:
r0,r1, … kind"same" | "added" | "removed" | "changed"requiredleftLabelstring | nullrequiredrightLabelstring | nullrequiredleftClauseIdstring | nullrequiredrightClauseIdstring | nullrequiredheadingstringrequiredarticlestringrequiredleftPageinteger | nullrequiredrightPageinteger | nullrequiredleftDiffSegment[]required- The earlier version's words:
sameanddelsegments. rightDiffSegment[]required- The later version's words:
sameandinssegments. renumberedbooleanrequired- The clause has a different number in each version. A renumbering alone is not a change.
scorenumberrequired- 0 to 1Match confidence of the pair.
ClauseChange
One entry of the What changed list.
kind"added" | "removed" | "changed"requiredclauseIdstringrequiredheadingstringrequiredleftPageinteger | nullrequiredrightPageinteger | nullrequiredglossstring- One model-written sentence, present only when a model wrote it.
leftClauseIdstring | nullrightClauseIdstring | nullanchorstring- The row to jump to.
digeststring- Short hash of the changed words; a saved summary is reused only while it matches.
EvalRun
One stored scorecard. A figure the run could not measure is null, never zero.
idstring (uuid)requiredcreatedAtstring (date-time)requiredgitShastring | nullrequired- Short commit the run was made from.
modelstring | nullrequired- Model that wrote the answers; null when none ran.
judgeModelstring | nullrequired- Model that scored faithfulness and correctness.
retrievalOnlybooleanrequired- Only retrieval and the confidence gate ran: no answer model.
notesstring | nullrequired- What a reader needs to interpret the numbers, e.g. how the embeddings were made.
confidenceThresholdnumber | nullpromptVersionstring | nulljudgePromptVersionstring | nullquestionCountintegerrequiredoutOfScopeCountintegerrequiredpageHitAt1numberrequired- 0 to 1
pageHitAt3numberrequired- 0 to 1
clauseHitAt1numberrequired- 0 to 1
clauseHitAt3numberrequired- 0 to 1
mrrnumberrequired- 0 to 1Mean reciprocal rank.
declineAccuracynumber | nullrequired- 0 to 1Out-of-scope questions that ended in a decline.
gateDeclineAccuracynumber | nullrequired- 0 to 1Out-of-scope questions the gate stopped before any model call.
falseDeclineRatenumber | nullrequired- 0 to 1Answerable questions declined anyway.
faithfulnessnumber | nullrequired- 1 to 5Mean judge score: is every claim supported by the passages.
correctnessnumber | nullrequired- 1 to 5Mean judge score against the reference answer.
citationRatenumber | nullrequired- 0 to 1Answers whose every citation points at a passage the model saw.
latencyP50MsintegerrequiredlatencyP95MsintegerrequiredttftP50Msinteger | nullttftP95Msinteger | nullpromptTokensintegercompletionTokensintegertotalTokensintegerrequiredcostUsdnumberrequired- Billed cost of the answers; 0 on Workers AI.
EvalQuestionResult
idstringrequiredg01tog30(gold) oro01too10(out of scope).kind"gold" | "out_of_scope"requiredquestionstringrequireddocumentIdstringrequiredexpectedPageinteger | nullrequiredexpectedClausestring | nullrequiredretrievedobject[]required- The passages the model was shown, in rank order.
pageRankinteger | nullrequired- Rank of the first passage on the expected page.
clauseRankinteger | nullrequired- Rank of the first passage with the expected clause.
confidencenumberrequireddeclinedbooleanrequiredgateDeclinedbooleanrequiredpassbooleanrequiredanswerstringrequiredvalidCitationsintegerrequiredtotalCitationsintegerrequiredfaithfulnessinteger | nullrequiredcorrectnessinteger | nullrequiredjudgeNotestring | nullrequiredlatencyMsintegerrequiredttftMsintegerpromptTokensintegerrequiredcompletionTokensintegerrequiredcostUsdnumberproviderstringmodelstringerrorstring
Error
Every error body has this shape.
errorobjectrequirederror.code"invalid_request" | "invalid_id" | "not_found" | "unauthorized" | "forbidden" | "rate_limited" | "budget_exhausted" | "upstream_unavailable" | "payload_too_large" | "unsupported_media_type" | "not_a_pdf" | "scanned_pdf" | "encrypted_pdf" | "too_many_pages" | "too_many_uploads" | "internal"requirederror.messagestringrequired- Safe to show to a person.
error.requestIdstringerror.retryAfterSecondsintegerrate_limitedandbudget_exhausted: seconds until a request can succeed.error.resetsAtstring (date-time)budget_exhausted: when the daily budget resets (00:00 UTC).