Contracts-Version. A pinned key
never sees a breaking change; you move by changing the date and testing.
Status: pre-release. No version is stable yet. Shapes documented on this site are the contract we
intend to ship, written ahead of the endpoints existing.
Unreleased
2026-08-24
The public resource is
contract everywhere. The API used to call it a paper. Paths, field names,
event names and id prefixes now all say the same word, and no synonym survives on the surface.This is a breaking rename, and it is safe to make only because no version is stable yet.The word paper still appears in prose where it means the physical thing, as in a contract signed years
ago on paper. It is no longer the name of anything you can call.
2026-08-24
Two open questions on the webhooks surface came back as rulings.Registering a URL no longer makes it trusted. Before the
201 returns, we send the URL a signed
endpoint.verification ping carrying a nonce, and your handler echoes the nonce back in the body of a
2xx. Echo it and the endpoint is active. Anything else leaves it pending, and a pending endpoint
receives nothing at all — which is the point, because a typo used to look exactly like a working
endpoint. The window is five minutes, and verification.reason names which of the four ways it failed.POST /webhooks/endpoints/{id}/verifyretries the handshake with a fresh nonce.POST /webhooks/endpoints/{id}/testsends one delivery on demand, so a handler is testable before any contract moves.- The ping reaches you before the
201that carries the secret. That ordering is now stated on the page rather than left to be discovered.
import.failed and ingress.detected are org-stream events. They are the only two that carry no
contract, and they now have an answer rather than a gap. They carry no sequence field at all,
their scope is the org rather than a party row, and they are walked back through a new org-scoped door:GET /events?starting_after=<event_id>, oldest first, cursor is an event id rather than a sequence.
id, on every surface.2026-08-24
Nine reviewers read this API the way a cold developer would, and found it was written from the
sender’s chair. The moment somebody other than the sender had to act, the surface ran out. Everything
below is additive: no field was removed and no status changed.Knowing who you are, and letting the other side act
GET /me. Any valid credential, no scope required. It returns yourperson_id, the contracts you are a party to and what you may do on each, your scopes, and your effective rung. Every write names a person and nothing used to hand you one.- Guests write, and there is one form for it.
Authorization: Token <t>as a header on every guest write. The query parameter?t=is for reading a link and nothing else. The token names one contract and one person and supplies both, so a guest call sends neitherContracts-Acting-Personnorconfirmed_by. block_idonPOST /contracts/{id}/decisions. The other side can now raise the first question on a clause.ask_idandblock_idare mutually exclusive,block_idis valid only withtype: "counter", andwhystays required.POST /contracts/{id}/decisions/apply-playbook. Apply your standard positions across a whole letter in one call, withdry_runand a named reason for every ask it declined to touch. It stages only. The round still needs a person to send it.
GET /contracts/{id}/signaturesandGET /signatures/{id}. Full signature objects at last, struck ones included, each carrying the hash it really bound, the intent, the mark andconfirmed_by.required_signers[]is people, everywhere. It replacesrequired[], and each row joins that person’s signature and its state, so “who still has to sign” is one read and no joins.GET /contracts/{id}/certificate. The record is a place, not only a field on a webhook. Readable undercontracts:read, a signed URL good for fifteen minutes, plus the recipe anybody can check it with.- The mark:
GET,PUTandDELETE /people/{id}/mark. Ink belongs to the person, never the party. A signature pins the mark and its checksum at press time, so re-drawing it later cannot change what is already signed.
- Endpoint management over HTTP. Register, list, read, delete,
POST …/rotate(dual-signed for a 24-hour overlap) andPOST …/replay. The secret is shown once, on create and on rotate. Registering an endpoint is no longer a dashboard-only act. - Seven new events, and
signature.voidsettles a spelling that two surfaces disagreed on. The new ones:letter.imported,contract.cancelled,signature.held,signature.bound,ask.withdrawn,party.changedandingress.detected. Every state a signature can reach now has an event. - Dedupe on the event
id, and only on theid.sequenceis demoted to an ordering hint that may arrive late. The old advice, to drop anything at or below the last sequence you saw, silently lost retried events and is gone. - Catching up after an outage.
GET /contracts/{id}/eventswalks one contract’s log, rendered on your own seat.GET /contracts?updated_since=finds which contracts moved at all.
filedis a seventh contract state, for a document that was signed somewhere else. Readable, searchable and citable; never negotiable and never signable.POST /contracts/{id}/amendis the door out of it.emailis a seventhorigin, with real endpoints behind it:GET /ingress/address,GET /ingress,POST /ingress/{id}/acceptandPOST /ingress/{id}/reject. When a contract is both forwarded and imported,emailwins.words_hashcomes back on every mutating response. No extraGETafter a write to find the value the nextIf-Matchneeds.Idempotency-Keyis widened to every POST that creates or moves a record, which now includesPOST /contracts,POST /importsandPOST /assets. One timed-out 100-file import used to mint a hundred duplicate contracts, and there is no bulk delete.If-Match, stated once and truthfully. Required only on calls that can move words or commit a round. Accepted and ignored on edits to your own private draft. Not applicable where there is no contract hash yet. The full table is on Authentication.
2026-08-24
The drafts were reconciled and seventeen disagreements were ruled on. Nothing below is a new
capability; each item settles a place where
API-DRAFT.md, AGENT-HARNESS.md and this site said
different things.Endpoints named at last. All of these were being called by the agent harness with nothing
documented behind them:GET /contracts/{id}/state: the composite read, blessed as public. The MCP server’sget_room_state.GET /contracts/{id}/asks,GET /contracts/{id}/letters/{letter_id},GET /contracts/{id}/signatures/state.GET /contracts/{id}/receipts: receipts, never “versions.” The harness tool renamed to match.PATCH /contracts/{id}/reply: the reply note before commit, whereset_reply_notelands.GET /playbooksis read-only, with a documented shape:rule_text,floor,ceiling,preferred,source.
- One import path.
POST /importsis canonical; thePOST /assets→POST /contracts/importtwo-step is superseded and will not ship.POST /assetsstays, for files that are not becoming contracts. import.completedandimport.failedjoined the webhook table. The count published alongside them was wrong and is corrected in the platform pass above: eighteen event types.If-Matchis a header everywhere. Theif_matchbody field is gone from the reply example.- Every error code carries a status.
403for the two seat errors,409for the four conflicts. - Two new scopes:
imports:writeandask:read, both split out of broader grants. POST /askdocumentsstream: falseas a first-class JSON shape alongside SSE.blocks/{id}/confirmis a settle, not a write. A parse-proposed value only, before first Send only.- Signature states are four everywhere:
held · bound · void · withdrawn. - The agent tool list standardized on
create_contract(draft_contractis an undocumented alias). prepare_signatureis exposed over MCP, pointer-only. No name, no intent, no hash to bind.- The parity rule is refined: it binds acting verbs. The explain-and-scan tools are compositions over public reads and need no endpoints; that is the boundary working, not a gap.
2026-08-24
Initial public surface.
- Contracts: draft, read, read whole state, list with
originfilter, set and confirm facts, send, invite, remove party, cancel, amend, read clause threads. - Replies: read letters, list asks, stage and withdraw decisions, read, note and discard the unsent reply, commit a round, withdraw a sent ask.
- Signing: create and withdraw signatures bound to a canonicalized text hash, read signature state, read receipts.
- Imports:
POST /importswith batching, per-import status, unverified spans, block confirmation. - Ask:
POST /askover the corpus, streaming or whole, citation-first. - Attention:
GET /attention, grouped by relationship. - Webhooks: eighteen event types, HMAC-SHA256 with a five-minute tolerance, and a per-contract
sequenceon every delivery.
confirmation_requiredon every send-class call. No scope, key, or setting removes it.If-Matchon the calls that move words or commit a round;words_changedcarriescurrent_hashandre_asked[].originset once at drafting, never patchable, never read by permissions.- No direct clause-text write.
PUT /contracts/{id}/blocks/{id}does not exist and will not.
Still moving
Flagged here rather than documented as settled. Six of the seven entries that stood here on 2026-08-24 were ruled on and moved into the reconciliation above: two import paths, the import events, theif_match body field, the GET /playbooks shape, version reads, and the parity gaps.
The seventh is settled too. Webhook endpoint management has shapes now, in the platform pass above:
register, rotate and replay are all HTTP calls, the secret is shown once, and you no longer have to go
to the dashboard to make one. See Managing endpoints.
Nothing is outstanding here today. Two things are deliberate rather than unresolved, and are recorded
here so nobody re-opens them as bugs:
- One
409 words_changedfor both stale cases, with no412. A staleIf-Matchand a stale signaturetext_hashland on the same code with the samecurrent_hashpayload, so one branch in a caller’s code handles every “the contract moved under you” case. See Errors. - The explain-and-scan tools have no endpoints.
explain_clause,summarize_letter,what_changed,risk_scanandcheck_readyare compositions over public reads. Parity binds acting verbs; prose is not a resource. See the parity principle.
Versioning policy
1
Additive changes ship immediately
New fields, new endpoints, new webhook events, and new enum values can appear on any date. Your
client must tolerate unknown fields and unknown event types.
2
Breaking changes get a new date
Removing a field, renaming one, changing a type, or changing a status code requires a new
Contracts-Version. Your key stays pinned until you move it.3
Behaviour changes count as breaking
A change to when
words_changed fires, or to what lands in re_asked[], is a version change even
though no field moved.The one thing that will never change under any version: no key sends or signs on its own authority.
If you ever see a send succeed without
confirmed_by, that is a security report, not a new feature.