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

# Errors

> One envelope for every failure. confirmation_required is a wall, words_changed is a compare-and-swap, and a seat error is a fact about the contract.

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

Every error returns the same shape. There are no exceptions, including the signing conflict, which
carries two extra fields **beside** the envelope and never instead of it.

```json theme={null}
{
  "error": {
    "type": "conflict_error",
    "code": "confirmation_required",
    "message": "A person must confirm this.",
    "doc_url": "https://docs.contracts.io/errors#confirmation-required",
    "param": "confirmed_by"
  }
}
```

<ResponseField name="code" type="string">
  The stable, machine-readable identifier. **Branch on this**, never on `message`.
</ResponseField>

<ResponseField name="type" type="string">
  The coarse family, for logging and for deciding whether a whole class is worth retrying. One of
  `invalid_request_error`, `authentication_error`, `permission_error`, `conflict_error`,
  `rate_limit_error`, `api_error`.
</ResponseField>

<ResponseField name="param" type="string">
  The field at fault, when there is one.
</ResponseField>

<ResponseField name="doc_url" type="string">
  A link into this page. Surface it in your own error UI.
</ResponseField>

<Note>
  **`doc_url` has one format**: `https://docs.contracts.io/errors#` followed by the code with its
  underscores turned into hyphens. `words_changed` becomes `.../errors#words-changed`. Every code is a
  section on this one page. There is no page per code, so a link built any other way is a dead link in
  your product.
</Note>

## The state codes

These six are the ones that mean the negotiation itself disagreed with you. They are the codes worth
writing real handling for.

| Code                         | Status | Type               | Means                                                                                                              |
| ---------------------------- | ------ | ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `confirmation_required`      | `409`  | `conflict_error`   | A send-class call arrived without `confirmed_by`.                                                                  |
| `words_changed`              | `409`  | `conflict_error`   | Your `If-Match` hash, or a signature's `text_hash`, is stale.                                                      |
| `not_a_negotiator`           | `403`  | `permission_error` | The acting person is on this contract, without negotiating capacity.                                               |
| `not_a_signer`               | `403`  | `permission_error` | The acting person is on this contract, without signing capacity.                                                   |
| `not_on_required_party_list` | `403`  | `permission_error` | The acting person is not a party to this contract at all.                                                          |
| `ask_voided_by_counter`      | `409`  | `conflict_error`   | The ask you decided on was superseded by a counter.                                                                |
| `contract_executed`          | `409`  | `conflict_error`   | The contract is signed and done. Amend it instead.                                                                 |
| `contract_filed`             | `409`  | `conflict_error`   | The contract is a record of something signed somewhere else. Read it and cite it; you cannot negotiate or sign it. |
| `contract_cancelled`         | `409`  | `conflict_error`   | Somebody killed this deal.                                                                                         |

## The codes you hit while wiring up

Nothing on this list is interesting once your integration works. All of it is what the first afternoon
looks like, so it belongs on the page rather than in a support thread.

| Code                     | Status                      | Type                    | Means                                                                                                                                                                                                                              |
| ------------------------ | --------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unauthorized`           | `401`                       | `authentication_error`  | No key, a malformed key, a revoked key, or a guest link that expired or was revoked.                                                                                                                                               |
| `insufficient_scope`     | `403`                       | `permission_error`      | The key works and this scope was never granted. `param` names the missing scope.                                                                                                                                                   |
| `acting_person_required` | `400`                       | `invalid_request_error` | An org key wrote, or read `GET /attention`, without `Contracts-Acting-Person`.                                                                                                                                                     |
| `if_match_required`      | `428`                       | `invalid_request_error` | A word-moving call arrived with no `If-Match` at all. Different from `words_changed`, which means you sent one and it was out of date.                                                                                             |
| `invalid_request`        | `400`                       | `invalid_request_error` | Something is malformed and `param` says what. A prompt and a `template_id` together, an `ask_id` and a `block_id` together, a missing `why`, a guest call carrying `confirmed_by`, a confirm carrying `text` after the first send. |
| `idempotency_error`      | `400`                       | `invalid_request_error` | You reused an `Idempotency-Key` with a different body.                                                                                                                                                                             |
| `idempotency_in_flight`  | `409`                       | `conflict_error`        | The first call under this key has not finished. Carries `Retry-After`.                                                                                                                                                             |
| `not_found`              | `404`                       | `invalid_request_error` | No such record, **or** a record this seat may not see. Deliberately the same answer for both.                                                                                                                                      |
| `fact_has_no_span`       | `400`                       | `invalid_request_error` | You set a fact whose value appears nowhere in the words. A fact the words do not carry is metadata, and this API has none.                                                                                                         |
| `not_a_parse_candidate`  | `400`                       | `invalid_request_error` | A confirm named text the parse never proposed for that spot.                                                                                                                                                                       |
| `unsupported_format`     | `415`                       | `invalid_request_error` | An import file outside `docx`, `pdf`, `txt`, `md`.                                                                                                                                                                                 |
| `file_too_large`         | `413`                       | `invalid_request_error` | One file over 25MB.                                                                                                                                                                                                                |
| `too_many_files`         | `400`                       | `invalid_request_error` | More than 100 files in one import call.                                                                                                                                                                                            |
| `generation_failed`      | `200` then an `error` event | `api_error`             | An answer from `POST /ask` stopped partway. Nothing was saved.                                                                                                                                                                     |
| `rate_limited`           | `429`                       | `rate_limit_error`      | Over the limit. Carries `Retry-After`.                                                                                                                                                                                             |

<Note>
  **`404` is the privacy answer.** A contract you are not on and a contract that never existed return exactly
  the same body. If they differed, somebody outside could guess ids and sort the real ones from the
  invented ones just by reading the status. The one exception is a contract you **can** see where you lack
  the capacity for the act, which is a `403`, because on a contract you hold, who is required for what is
  public to both sides.
</Note>

## When an import fails

`import.failed` carries a `reason` from a closed list, so you can count and route a hundred-file batch
without reading prose.

| `reason`             | Means                                                                                |
| -------------------- | ------------------------------------------------------------------------------------ |
| `unsupported_format` | Not one of `docx`, `pdf`, `txt`, `md`.                                               |
| `file_too_large`     | Over 25MB.                                                                           |
| `extraction_empty`   | The file opened and had no text in it. A scan with no text layer usually lands here. |
| `not_a_contract`     | Text came out and it was not an agreement.                                           |
| `parser_error`       | We broke. Retry with a fresh idempotency key.                                        |
| `virus_detected`     | The file was rejected before anything read it.                                       |

***

## `confirmation_required`

<Warning>
  **Status `409`.** This is the never-sends rule as a status code, and it is the whole agent story in one
  response.
</Warning>

Returned by `POST /contracts/{id}/send`, `POST /contracts/{id}/replies`, and
`POST /contracts/{id}/signatures` when `confirmed_by` is absent.

```json 409 theme={null}
{
  "error": {
    "type": "conflict_error",
    "code": "confirmation_required",
    "message": "A person must confirm this. Re-send with confirmed_by set to the person_id of the human who pressed the button.",
    "param": "confirmed_by",
    "doc_url": "https://docs.contracts.io/errors#confirmation-required"
  },
  "letter_preview": { "from": "...", "to": ["..."], "decisions": [ /* ... */ ] },
  "landed_if_sent": []
}
```

The response carries a **fully assembled letter preview**. Everything a person needs to look at before
deciding. That is the point: the call did not fail, it arrived at a human.

**Recovering:** put `letter_preview` in front of a person. When they press the button, call again with
`confirmed_by` set to their `per_…`.

<Warning>
  **Do not retry this.** It is not a rate limit and not a transient failure. It is the end of the road,
  and the next actor is a person. There is no flag, key, scope, rung, or org setting that removes it.
  Holding the `send` and `sign` scopes does not exempt a key.
</Warning>

***

## `words_changed`

<Warning>
  **Status `409`.** The generalized compare-and-swap. Every mutating call carries
  `If-Match: <contract.words_hash>`; a stale hash lands here.
</Warning>

```json 409 on a reply 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"]
}
```

```json 409 on a signature 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..."
  }
}
```

<ResponseField name="your_signature" type="object | null">
  The signer's **previous** signature on this contract, if they had one, with its true state. When the
  words moved under it, that state is `void`. When this signer had no earlier signature,
  `your_signature` is `null`.

  The press you just submitted is always rejected outright. Nothing is half-created, so there is never
  a signature in some fifth in-between state to clean up.
</ResponseField>

**Recovering:**

<Steps>
  <Step title="Take the new hash">
    `current_hash` is the value to send as `If-Match` next time.
  </Step>

  <Step title="Re-decide only what was invalidated">
    `re_asked[]` names the decisions a concurrent letter killed. Everything not listed is still staged
    and still good. Do not rebuild the whole round.
  </Step>

  <Step title="For signatures, collect a fresh one">
    A signature survives a counter as `held`; it voids only when the words mint a new hash. The
    replacement is always a **fresh** `POST /contracts/{id}/signatures` against the hash the signer read.
    Never migrate consent onto text nobody saw.
  </Step>
</Steps>

<Note>
  **One `409` covers both stale cases, and there is no `412`** (reaffirmed 2026-08-24). This note stands
  deliberately and is not an oversight to be tidied away.

  The two cases are a stale `If-Match` on a mutating call and a stale `text_hash` on a signature. A
  `412 Precondition Failed` would be the conventional status for the first; this API declines it so that
  **one** branch in your code handles every "the contract moved under you" case with the same
  `current_hash` payload. Two statuses for one condition would buy protocol tidiness and cost a caller
  the single recovery path.
</Note>

***

## Seat errors: `403`

Two codes, and the difference is worth reading carefully.

<ResponseField name="not_on_required_party_list" type="403">
  The acting person is **not a party to this contract at all.** They cannot read it, decide on it, or sign
  it. This is not a missing scope. It is a fact about the contract.
</ResponseField>

<ResponseField name="not_a_negotiator" type="403">
  The acting person **is** on the contract, but without `negotiator` capacity. A `signer`-only party can
  read and sign; they cannot stage or send decisions.
</ResponseField>

<ResponseField name="not_a_signer" type="403">
  The acting person **is** on the contract, but without `signer` capacity. A negotiator-only party can
  counter and reply all day; they cannot press. This also fires when somebody tries to withdraw a
  signature that is not theirs, because only the person who pressed it, or their own org acting for
  them, may take it back.
</ResponseField>

**Do not "fix" these by widening a key's scopes.** Capacity comes from the contract's required-party list,
not from the API key, and it is public. Both sides see who is required for what. The fix is a
`POST /contracts/{id}/invite` performed by someone who holds the authority to invite.

<Note>
  A `403` here never leaks org **authority**. The counterparty must never learn that Acme made its junior
  get a review, so error messages describe capacity, which is public, and never authority, which is not.
</Note>

***

## `ask_voided_by_counter`: `409`

You decided on an ask that a counter has already superseded. A counter voids all prior accepts, sent and
unsent, by resetting the unanimity window.

**Recovering:** re-fetch `GET /contracts/{id}/letters/latest`, find the live ask on that block, and decide
on that one instead.

***

## `contract_executed`: `409`

The contract is terminal. No further decisions, signatures, invites or cancellations are possible.

**Recovering:** `POST /contracts/{id}/amend` mints a **new** contract with the same parties.

***

## Rate limits: `429`

| Class           | Limit      |
| --------------- | ---------- |
| Reads           | 1000 / min |
| Writes          | 120 / min  |
| `send` + `sign` | 20 / min   |

Per org, and agent traffic counts against the **acting person**. A runaway harness throttles one
seat, not the company. A `429` carries `Retry-After`; honour it with backoff.

`429` is the one status on this page that **is** safe to retry.

## `contract_filed`: `409`

This contract is a record of an agreement that was signed somewhere else, before it ever reached us. You
can read it, search it and cite it. You cannot send it, negotiate it or sign it, because our signing
surface can only bind our own copy of the words, and our copy is not what anybody put their name to.

**Recovering:** if you want to renew or vary it, `POST /contracts/{id}/amend` mints a fresh negotiable
contract that points back at this one. That is how a renewal of something signed years ago on paper
begins. See [Imports](/api/imports#already-signed-documents).

***

## Idempotency

`Idempotency-Key` is required on every POST that creates or moves a record: `send`, `replies`,
`signatures`, `POST /contracts`, `POST /imports` and `POST /assets`. Replays return the original response,
so a network timeout on a send is safe to repeat with the same key.

| Situation                                      | What you get                                    |
| ---------------------------------------------- | ----------------------------------------------- |
| Same key, same body                            | The original response.                          |
| Same key, different body                       | `400 idempotency_error`.                        |
| Same key while the first call is still running | `409 idempotency_in_flight` with `Retry-After`. |
| Same key after 24 hours                        | Treated as new. Keys are kept for 24 hours.     |

<Warning>
  **One key per attempt, not one per candidate.** Reusing a key across two genuinely different sends is
  the failure worth designing against: either the second call replays the first response, so people you
  never wrote to look like they were written to, or it fails with `idempotency_error` and your handler
  has never seen that code.
</Warning>

<Warning>
  Retrying a **`confirmation_required`** with the same idempotency key does not help. Nothing was
  recorded, because nothing happened. A person still has to press the button.
</Warning>
