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.string
The stable, machine-readable identifier. Branch on this, never on
message.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.string
The field at fault, when there is one.
string
A link into this page. Surface it in your own error UI.
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.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.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.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.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.
confirmation_required
Returned by POST /contracts/{id}/send, POST /contracts/{id}/replies, and
POST /contracts/{id}/signatures when confirmed_by is absent.
409
letter_preview in front of a person. When they press the button, call again with
confirmed_by set to their per_….
words_changed
409 on a reply
409 on a signature
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.1
Take the new hash
current_hash is the value to send as If-Match next time.2
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.3
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.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.Seat errors: 403
Two codes, and the difference is worth reading carefully.
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.
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.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.POST /contracts/{id}/invite performed by someone who holds the authority to invite.
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.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
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.
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.