> ## 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.

# Quickstart

> Draft a contract, send it, take a counter from the other side, commit a reply, and sign. A curl sequence with the If-Match and words_hash flow shown at every step.

<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>

Eight steps, twelve calls, and a whole negotiation from nothing to signed. Both sides are here, because
half of it happens in somebody else's hands. The shape is always the same: **find out who you are,
draft, send, one reply cycle, sign.**

<Info>
  Set these once. Every call below assumes them.

  ```bash theme={null}
  export CONTRACTS_KEY="sk_test_..."
  export API="https://api.contracts.io"
  ```
</Info>

## 1. Find out who you are

Every write names the person it acts for, and you do not have that person's id yet. This is the call
that hands it to you. Make it first.

```bash theme={null}
curl $API/me \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Version: 2026-08-24"
```

```json 200 OK theme={null}
{
  "person_id": "per_9",
  "name": "Nora Whitfield",
  "email": "nora@acme.com",
  "org_id": "org_2",
  "org_name": "Acme",
  "parties": [],
  "person_rung": 1,
  "org_ceiling": 2,
  "effective_rung": 1,
  "scopes": ["contracts:read", "contracts:write", "decisions:write", "send", "sign"],
  "pinned_version": "2026-08-24",
  "credential": "org_key",
  "is_guest": false
}
```

```bash theme={null}
export ACTING="per_9"        # the human this key acts for
```

<Note>
  A test key with no `Contracts-Acting-Person` header comes back with `person_id: null`. That is a key
  that can read across your org and write nothing. Bind it to a person in **Settings, Developers**, or
  send the header on every call, as we do below.
</Note>

## 2. Draft a contract

Give a `prompt` **or** a `template_id`, not both.

```bash theme={null}
curl -X POST $API/contracts \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Version: 2026-08-24" \
  -H "Contracts-Acting-Person: $ACTING" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "6-month contract PM at £600/day, send to sam@acme.com",
    "facts": { "governing_law": "England & Wales", "term_months": 6 }
  }'
```

```json 201 Created theme={null}
{
  "id": "con_1",
  "state": "draft",
  "origin": "api",
  "url": "https://contracts.io/c/con_1",
  "words_hash": "sha256:1a4d...",
  "head_sequence": 1,
  "blocks": [ { "id": "blk_1", "number": 1, "kind": "clause", "text": "..." } ],
  "facts": [
    { "id": "fct_law", "name": "governing_law", "value": "England & Wales",
      "inferred": false, "occurrences": ["blk_12"] },
    { "id": "fct_term", "name": "term_months", "value": "6 months",
      "inferred": false, "occurrences": ["blk_3", "blk_9"] }
  ],
  "questions": [ { "id": "q_1", "asks": "Who are the parties?" } ],
  "proposed_recipients": [
    { "email": "sam@acme.com", "capacity": ["negotiator", "signer"] }
  ]
}
```

<Warning>
  Your prompt named a recipient, so you got **`proposed_recipients`**. Never a sent contract. That is the
  API's version of *a Send sheet, addressed, one button unpressed.* Nothing has left your side.
</Warning>

`questions[]` is capped at three and only ever contains genuine forks: things the draft cannot resolve
without you, not a form to fill in.

**Keep `words_hash`.** Every call that changes anything returns it, and it is what you send as
`If-Match` on the next one. You never need an extra `GET` to find it.

## 3. Fill a fact, before the first send

Before the first send you hold the words alone, so a fact change **applies directly**. `fct_term` came
back in the response above.

```bash theme={null}
curl -X POST $API/contracts/con_1/facts/fct_term \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING" \
  -H "If-Match: sha256:1a4d..." \
  -H "Content-Type: application/json" \
  -d '{ "value": "9 months" }'
```

```json 200 OK theme={null}
{ "applied": true, "blocks_touched": ["blk_3", "blk_9"], "words_hash": "sha256:6b02..." }
```

The hash moved. **Carry the new one into the next call.** After the first send this same request
returns `"applied": false` and stages a proposal instead, and the hash comes back unchanged, because a
proposal moves no words.

## 4. Send, and the room is born

Send is confirmation-class. `confirmed_by` is the `per_…` of the human who pressed the button, and the
server checks that they are really on this contract and really allowed to do this.

```bash theme={null}
curl -X POST $API/contracts/con_1/send \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING" \
  -H "If-Match: sha256:6b02..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "email": "sam@acme.com", "name": "Sam Ford", "capacity": ["negotiator", "signer"] }
    ],
    "note": "First draft. Shout if the cap is wrong.",
    "confirmed_by": "per_9"
  }'
```

```json 200 OK theme={null}
{
  "state": "sent",
  "letter": "let_1",
  "relationship": "rel_4",
  "relationship_created": true,
  "words_hash": "sha256:6b02..."
}
```

`relationship_created: true` is the only trace of a room being born. There was no `POST /rooms`, no
routing order, no recipient roles configured before words moved, and no "send for signature" mode.
Capacity did that work.

<Warning>
  Drop `confirmed_by` and you get `409 confirmation_required`. Do not retry it. Assemble the letter,
  put it in front of a person, and call again once they have pressed the button.
</Warning>

## 5. The other side counters

Sam has no account with us. He got an email with a link, and he can negotiate and sign through it. This
is the normal way the other side arrives, so it is worth doing once here rather than discovering it in
production.

**Reading uses the link**, with the token as a query parameter:

```bash theme={null}
curl "$API/contracts/con_1?t=gtk_8f3a..."
```

**Writing uses a header.** Sam wants nine months instead of twelve, and nobody has raised that question
yet, so he names the clause rather than a question:

```bash theme={null}
curl -X POST $API/contracts/con_1/decisions \
  -H "Authorization: Token gtk_8f3a..." \
  -H "If-Match: sha256:6b02..." \
  -H "Content-Type: application/json" \
  -d '{
    "block_id": "blk_9",
    "type": "counter",
    "text": "nine (9) months",
    "why": "We cannot commit past the end of our financial year."
  }'
```

```json 201 Created theme={null}
{
  "id": "dec_11", "state": "staged", "sent": false,
  "ask": "ask_12", "ask_created": true, "words_hash": "sha256:6b02..."
}
```

Then he commits the round:

```bash theme={null}
curl -X POST $API/contracts/con_1/replies \
  -H "Authorization: Token gtk_8f3a..." \
  -H "If-Match: sha256:6b02..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "note": "One number, otherwise happy." }'
```

<Note>
  **A guest call sends no `Contracts-Acting-Person` and no `confirmed_by`.** The link token names one
  contract and one person, so it already supplies both. Sending either one returns `400 invalid_request`
  rather than being quietly believed. Everything else works exactly as it does for a key: same
  `If-Match`, same `Idempotency-Key`, same conflict responses. See
  [Guests](/api/authentication#guests-reading-and-writing-through-a-link).
</Note>

Use `block_id` when nobody has raised the question yet, and `ask_id` once somebody has. Send one or the
other, never both.

## 6. Read the inbound round and answer it

`letter.received` fires on your side. Fetch the round.

```bash theme={null}
curl $API/contracts/con_1/letters/latest \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING"
```

```json 200 OK theme={null}
{
  "id": "let_2",
  "kind": "sent",
  "sequence": 4,
  "from": "pty_acme",
  "note": "One number, otherwise happy.",
  "asks": [
    {
      "id": "ask_12",
      "block": "blk_9",
      "type": "replace",
      "standing": "twelve (12) months",
      "proposed": "nine (9) months",
      "why": "We cannot commit past the end of our financial year.",
      "tally": { "agreed": ["pty_acme"], "awaiting": ["pty_north"] }
    }
  ]
}
```

The letter came `from: "pty_acme"`, so **`pty_acme` is in `agreed`**. A party agrees by proposing.
Unanimity is only countable if the proposer is counted, so never read `agreed` as "everyone but them".

Nine months is fine. Stage an accept. Staging writes to **your unsent reply**, and nothing leaves your
side. The other party cannot see it, count it, or be told about it.

```bash theme={null}
curl -X POST $API/contracts/con_1/decisions \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING" \
  -H "If-Match: sha256:6b02..." \
  -H "Content-Type: application/json" \
  -d '{ "ask_id": "ask_12", "type": "accept" }'
```

```json 201 Created theme={null}
{ "id": "dec_14", "state": "staged", "sent": false,
  "ask": "ask_12", "words_hash": "sha256:6b02..." }
```

Inspect the round before committing it, and back out anything you change your mind about:

```bash theme={null}
# your staged decisions, your note, and the server's own dry run
curl $API/contracts/con_1/reply \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING"

# withdraw one decision
curl -X DELETE $API/contracts/con_1/decisions/dec_14 \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING"

# discard the whole round
curl -X DELETE $API/contracts/con_1/reply \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING"
```

`GET /contracts/{id}/reply` carries `landed_if_sent`, which is the server working out what would actually
change if you sent this. Never compute that yourself.

<Note>
  `why` is required on a counter and on a decline. A counter without a reason is a fight.
</Note>

## 7. Commit the reply

```bash theme={null}
curl -X POST $API/contracts/con_1/replies \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING" \
  -H "If-Match: sha256:6b02..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Nine works.", "confirmed_by": "per_9" }'
```

```json 200 OK theme={null}
{
  "letter": "let_3",
  "state": "agreed",
  "landed": ["blk_9"],
  "re_asked": [],
  "asks_opened": [],
  "words_hash": "sha256:9f2c..."
}
```

Three fields carry the whole result:

* **`landed`** are the clauses that reached unanimity on this send. **This is the only way words ever
  change.** `blk_9` now reads "nine (9) months", and the hash moved because of it.
* **`asks_opened`** are questions this round put on the table. Empty here, because you accepted rather
  than countered. **A counter always returns `landed: []`**: it opens a question and waits, it does not
  move words.
* **`re_asked`** are decisions killed because a letter from the other side moved that clause while your
  round was staged. Re-decide those, and only those, then commit again.

`state` is now `agreed`. Everyone has said yes to every clause, `contract.agreed` fires, and the signing
block wakes up.

If the hash you sent was stale, you get `409 words_changed` instead, with the current hash and a
non-empty `re_asked`. Refetch, re-decide, retry.

```json 409 words_changed theme={null}
{
  "error": {
    "type": "conflict_error",
    "code": "words_changed",
    "message": "The words moved while you were deciding.",
    "param": "If-Match",
    "doc_url": "https://docs.contracts.io/errors#words-changed"
  },
  "current_hash": "sha256:be71...",
  "re_asked": ["ask_7"]
}
```

## 8. Sign

<Warning>
  **Wait for `agreed` before you try this.** Signing an unfinished contract is refused. Step 7 returned
  `"state": "agreed"`, and `contract.agreed` fired on both sides. If you are polling instead of listening,
  `GET /contracts/con_1/state` is the check.
</Warning>

A signature binds to an **exact text hash**, never to "the contract". Each person signs for themselves.
You sign as your own signer, using your own key:

```bash theme={null}
curl -X POST $API/contracts/con_1/signatures \
  -H "Authorization: Bearer $CONTRACTS_KEY" \
  -H "Contracts-Acting-Person: $ACTING" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "text_hash": "sha256:9f2c...",
    "signer": { "name": "Nora Whitfield", "email": "nora@acme.com" },
    "confirmed_by": "per_9",
    "intent": "I agree to be bound by these words."
  }'
```

```json 201 Created theme={null}
{
  "id": "sig_2", "state": "bound", "version": "ver_5", "executed": false,
  "signed_at": "2026-08-24T11:04:00Z", "words_hash": "sha256:9f2c..."
}
```

Sam signs through his own link, from his own browser:

```bash theme={null}
curl -X POST $API/contracts/con_1/signatures \
  -H "Authorization: Token gtk_8f3a..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "text_hash": "sha256:9f2c...",
    "signer": { "name": "Sam Ford", "email": "sam@acme.com" },
    "intent": "I agree to be bound by these words."
  }'
```

<Warning>
  **You cannot sign for the person you sent to.** The person in `confirmed_by` must be the person
  signing, must hold signing capacity, and must be in your own org. Anything else returns
  `403 not_a_signer`. Consent is not something one side supplies on behalf of the other, and the API
  enforces that rather than trusting it.
</Warning>

If the words moved while a signer was reading, the compare-and-swap catches it:

```json 409 words_changed theme={null}
{
  "error": {
    "type": "conflict_error",
    "code": "words_changed",
    "message": "The words moved. Read the contract again and sign the current text.",
    "param": "text_hash",
    "doc_url": "https://docs.contracts.io/errors#words-changed"
  },
  "current_hash": "sha256:be71...",
  "your_signature": { "id": "sig_1", "state": "void", "bound_hash": "sha256:9f2c..." }
}
```

<Warning>
  Never migrate a signature onto new text. A signature survives an open question as `held`. It voids
  only when the words mint a new hash, and the replacement is always a **fresh**
  `POST /contracts/{id}/signatures` against the hash that signer actually saw.
</Warning>

When the last required signature binds, `contract.executed` fires with a `version` and a
`certificate_url`, and you can fetch the record any time from
[`GET /contracts/{id}/certificate`](/api/signing#the-certificate). `executed` is terminal. To
change it, `POST /contracts/{id}/amend`.

## The hash flow, in one picture

```
GET  /me                     → per_9
POST /contracts                 → words_hash: 1a4d
  ↓ If-Match: 1a4d
POST …/facts/fct_term        → applied, words_hash: 6b02
  ↓ If-Match: 6b02
POST …/send                  → letter let_1, relationship born
  ↓ If-Match: 6b02
POST …/decisions   [guest]   → ask_12 minted, staged (private, no hash move)
POST …/replies     [guest]   → letter let_2, landed[], asks_opened: [ask_12]
  ↓ If-Match: 6b02
POST …/decisions             → dec_14 staged (private, no hash move)
  ↓ If-Match: 6b02
POST …/replies               → landed: [blk_9], state agreed, words_hash: 9f2c
  ↓ text_hash: 9f2c
POST …/signatures            → sig_2 bound → ver_5 → executed
```

Staging never moves the hash. Only a **landed** clause does. That is why you can stage a whole round
without racing the other side, and why the race, when it happens, surfaces exactly once: at the commit.

## Next

<CardGroup cols={2}>
  <Card title="Every write and its headers" icon="table" href="/api/authentication#every-write-and-the-three-headers-it-takes">
    Which calls take `If-Match`, `Idempotency-Key` and `confirmed_by`.
  </Card>

  <Card title="Errors" icon="triangle-alert" href="/api/errors">
    Every code, its status, and how to recover.
  </Card>

  <Card title="Endpoint reference" icon="code" href="/api/contracts">
    Every public call, grouped by resource.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/api/webhooks">
    Eighteen event types, HMAC signing, and how to catch up after an outage.
  </Card>
</CardGroup>
