Step zero: find out who you are
Every write names the person it acts for. Before you can write anything, you need that person’s id, and this is the call that hands it to you. Make it first, every time, with any credential.GET /me needs no scope. Any working credential may call it, which is the point: it is how you find
out what you are holding.
parties returns at most 50 rows with has_more and next_cursor. A person on four thousand contracts
is normal, and this call has to stay small. Use GET /contracts when
you want the whole list.
effective_rung is already the answer. It folds the person’s own rung, the org ceiling and any
per-contract arming into one number, so you never recompute it yourself. See
the autonomy ladder.
Base URL and version
/v1/ in the path. Versions are date-pinned per key and overridable per call:
Contracts-Version header uses the version pinned to the key.
Org keys
Secret keys are scoped to one org and sent as a bearer token.sk_test_… and live keys are sk_live_….
Per-person tokens
For third-party apps, use OAuth. The resulting token is the seat, and three separate things travel with it:
Capacity is not configurable by an admin. It is a fact about the contract, not a setting.
Guests: reading and writing through a link
A guest is a person with no account and no org. Somebody sent them a contract and they can negotiate it and sign it. That is not a lesser seat, it is the normal way the other side arrives. Reading uses the link. The token rides as a query parameter, so the link somebody was emailed just works:- A write never carries the token in the URL. Web addresses end up in server logs, in browser
history and pasted into support tickets. A write sent with
?t=and no header gets401 unauthorized. - The token names one contract and one person, and it supplies both. Do not send
Contracts-Acting-Person. Sending one gets400 invalid_request. - The token is the confirmation. Do not send
confirmed_byeither. The link was mailed to that person, so somebody holding it is somebody who opened it. Sendingconfirmed_bygets400 invalid_requestrather than being quietly believed. - The link buys exactly what the contract gives that person. If they were invited to negotiate, they
can counter and reply. If they were invited to sign, they can sign. Nothing reaches any other contract,
and
POST /askis never available to a guest. If-MatchandIdempotency-Keywork exactly as they do for a key. Same headers, same statuses, same409 words_changedbody when the words moved.- Removing a guest from the contract kills their link. An expired or revoked token returns
401 unauthorized, never403, so somebody who was removed never learns whether the contract is still there.
There is still no guest key issuance endpoint, ever. A link token is not a key. It has no scopes,
it cannot be listed, and it cannot be minted through the API. It is a contract and a person, and that is
the whole grant. Guests are also never issued MCP connections.
MCP agents inherit the connecting person
An MCP agent connects as the person who connected it and holds that person’s rung, capped by the org ceiling, and capped additionally at cannot send, cannot sign. One rule, no second permission model for agents.Scopes
imports:write and ask:read are new (reconciled 2026-08-24), and both are narrowing splits
rather than additions. assets:write was carrying two different powers: attaching a rate card, and
parsing a contract into a live contract. That is why importing got its own grant. ask:read came out of
contracts:read because a corpus-wide question crosses every contract the seat can open, and a key granted
to read one contract should not silently read across the org.Neither widens anything: retrieval and reads stay capped by the acting person’s row-level access. The
scope decides whether you may ask; the seat decides what can answer.send and sign are grantable. A key holding both still requires confirmed_by on the
call. The scope decides whether you may ask; the confirmation decides whether it happens. See
the never-sends 409.Rate limits
Per org, with agent traffic counted against the acting person. A runaway harness throttles one seat, not the company.
A
429 carries Retry-After.
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.
Concurrency
Calls that can move words or commit a round take the contract’s current words hash:409 words_changed with the new hash and the decisions that must be re-asked. It
is the signing compare-and-swap, generalized to every write. See Errors.
You never need an extra read to find that hash. Every call that changes anything returns
words_hash in its own response body. That value is what you send as If-Match on the next call.
Every write, and the three headers it takes
If-Match is required only on calls that can move words or commit a round. It is accepted and ignored
on edits to your own private draft, and it does not apply where there is no contract hash yet.
A word-moving call sent with no
If-Match at all returns 428 if_match_required. That is a different
failure from 409 words_changed, which means you sent one and it was out of date.
Signing is the one compare carried in the body.
POST /contracts/{id}/signatures takes text_hash
as a field instead of If-Match as a header, because there the hash is not a cache check. It is the
thing being signed. It goes onto the signature, onto the certificate, and it is what decides years
later whether that signature is still live. A value that ends up on a legal record belongs in the
body where a reader can see it. Sending If-Match to the sign endpoint returns
400 invalid_request, so nobody sends both and wonders which one won.What confirmed_by actually checks
confirmed_by names the person who pressed the button. It is checked on the server, not taken on
trust, on send, replies, signatures and signature withdrawal.
- The person is on the contract’s required-party list. If not:
403 not_on_required_party_list. - The person holds the right capacity: negotiating for send and replies, signing for signatures and
withdrawals. If not:
403 not_a_negotiatoror403 not_a_signer. - The person is in your own org. A key can never confirm as the other side.
- The value is written onto the letter, the signature and the certificate, so “who pressed this” stays answerable from the record itself.