Set these once. Every call below assumes them.
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.200 OK
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.2. Draft a contract
Give aprompt or a template_id, not both.
201 Created
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.
200 OK
"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.
200 OK
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.
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:201 Created
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.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.
200 OK
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.
201 Created
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.
why is required on a counter and on a decline. A counter without a reason is a fight.7. Commit the reply
200 OK
landedare the clauses that reached unanimity on this send. This is the only way words ever change.blk_9now reads “nine (9) months”, and the hash moved because of it.asks_openedare questions this round put on the table. Empty here, because you accepted rather than countered. A counter always returnslanded: []: it opens a question and waits, it does not move words.re_askedare 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.
409 words_changed
8. Sign
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:201 Created
409 words_changed
contract.executed fires with a version and a
certificate_url, and you can fetch the record any time from
GET /contracts/{id}/certificate. executed is terminal. To
change it, POST /contracts/{id}/amend.
The hash flow, in one picture
Next
Every write and its headers
Which calls take
If-Match, Idempotency-Key and confirmed_by.Errors
Every code, its status, and how to recover.
Endpoint reference
Every public call, grouped by resource.
Webhooks
Eighteen event types, HMAC signing, and how to catch up after an outage.