> ## Documentation Index
> Fetch the complete documentation index at: https://docs.contracts.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Dated, additive changes to the contracts.io API. Versions are pinned per key.

<Warning>
  **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](/glossary) maps them.
</Warning>

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.

<Info>
  **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.
</Info>

## Unreleased

<Update label="The noun is contract" description="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.

  | Was                                                          | Is                                                              |
  | ------------------------------------------------------------ | --------------------------------------------------------------- |
  | `/papers`, `/papers/{id}/…`                                  | `/contracts`, `/contracts/{id}/…`                               |
  | `paper_id`, `paper` in a payload core                        | `contract_id`, `contract`                                       |
  | `paper.drafted` `.parsed` `.agreed` `.executed` `.cancelled` | `contract.drafted` `.parsed` `.agreed` `.executed` `.cancelled` |
  | `pap_1`                                                      | `con_1`                                                         |
  | MCP `get_paper`, `list_papers`, `draft_paper`                | `get_contract`, `list_contracts`, `draft_contract`              |

  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.
</Update>

<Update label="The delivery handshake, and the org stream" description="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.
</Update>

<Update label="Platform pass" description="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](/api/authentication#every-write-and-the-three-headers-it-takes).
</Update>

<Update label="Reconciled" description="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 /assets` → `POST /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.
</Update>

<Update label="Draft" description="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.
</Update>

## 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](/api/webhooks#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](/api/errors#words-changed).
* **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](/api/introduction#the-parity-principle).

## Versioning policy

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Note>
  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.
</Note>
