Skip to main content
Draft — ahead of the backend; nothing here is live yet. This surface is written against the design, not against a running server. There is no api.contracts.io to call, no key to issue, and every response below is an illustration rather than a recording. It is published so the shape can be argued with before it is built.It also predates CONTRACT-SCHEMA v0 and still uses the older nouns — ask for Proposal, receipt for Version, relationship for Room. The glossary maps them.
The API is date-versioned per key and overridable per call with 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}/verify retries the handshake with a fresh nonce.
  • POST /webhooks/endpoints/{id}/test sends one delivery on demand, so a handler is testable before any contract moves.
  • The ping reaches you before the 201 that 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.
Dedupe is unchanged: on the event 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 your person_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 neither Contracts-Acting-Person nor confirmed_by.
  • block_id on POST /contracts/{id}/decisions. The other side can now raise the first question on a clause. ask_id and block_id are mutually exclusive, block_id is valid only with type: "counter", and why stays required.
  • POST /contracts/{id}/decisions/apply-playbook. Apply your standard positions across a whole letter in one call, with dry_run and a named reason for every ask it declined to touch. It stages only. The round still needs a person to send it.
Signing, and reading it back
  • GET /contracts/{id}/signatures and GET /signatures/{id}. Full signature objects at last, struck ones included, each carrying the hash it really bound, the intent, the mark and confirmed_by.
  • required_signers[] is people, everywhere. It replaces required[], 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 under contracts:read, a signed URL good for fifteen minutes, plus the recipe anybody can check it with.
  • The mark: GET, PUT and DELETE /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.
Webhooks
  • Endpoint management over HTTP. Register, list, read, delete, POST …/rotate (dual-signed for a 24-hour overlap) and POST …/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.void settles a spelling that two surfaces disagreed on. The new ones: letter.imported, contract.cancelled, signature.held, signature.bound, ask.withdrawn, party.changed and ingress.detected. Every state a signature can reach now has an event.
  • Dedupe on the event id, and only on the id. sequence is 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}/events walks one contract’s log, rendered on your own seat. GET /contracts?updated_since= finds which contracts moved at all.
Contracts, provenance and conventions
  • filed is a seventh contract state, for a document that was signed somewhere else. Readable, searchable and citable; never negotiable and never signable. POST /contracts/{id}/amend is the door out of it.
  • email is a seventh origin, with real endpoints behind it: GET /ingress/address, GET /ingress, POST /ingress/{id}/accept and POST /ingress/{id}/reject. When a contract is both forwarded and imported, email wins.
  • words_hash comes back on every mutating response. No extra GET after a write to find the value the next If-Match needs.
  • Idempotency-Key is widened to every POST that creates or moves a record, which now includes POST /contracts, POST /imports and POST /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’s get_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, where set_reply_note lands.
  • GET /playbooks is read-only, with a documented shape: rule_text, floor, ceiling, preferred, source.
Settled
  • One import path. POST /imports is canonical; the POST /assetsPOST /contracts/import two-step is superseded and will not ship. POST /assets stays, for files that are not becoming contracts.
  • import.completed and import.failed joined the webhook table. The count published alongside them was wrong and is corrected in the platform pass above: eighteen event types.
  • If-Match is a header everywhere. The if_match body field is gone from the reply example.
  • Every error code carries a status. 403 for the two seat errors, 409 for the four conflicts.
  • Two new scopes: imports:write and ask:read, both split out of broader grants.
  • POST /ask documents stream: false as a first-class JSON shape alongside SSE.
  • blocks/{id}/confirm is 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_contract is an undocumented alias).
  • prepare_signature is 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 origin filter, 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 /imports with batching, per-import status, unverified spans, block confirmation.
  • Ask: POST /ask over 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 sequence on every delivery.
Standing locks introduced with the surface
  • confirmation_required on every send-class call. No scope, key, or setting removes it.
  • If-Match on the calls that move words or commit a round; words_changed carries current_hash and re_asked[].
  • origin set 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, the if_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_changed for both stale cases, with no 412. A stale If-Match and a stale signature text_hash land on the same code with the same current_hash payload, 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_scan and check_ready are 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.