- Staged decisions are private. They live in your unsent reply and never leave your side until a human confirms a Send.
- You cannot write clause text. You propose. Words move when every required negotiator has accepted, and not before.
Read the latest letter
endpoint
Scopes:
contracts:readendpoint
Scopes:
contracts:readlatest is an alias, not the only door. Any committed round is addressable by its let_… id. Letters
are immutable, so a fetched letter never changes under you.
200 OK
standing words, the proposed words, the why, and a per-party tally. A grouped
ask touches several clauses at once: one fact change, one ask, one decision for the counterparty.
Read the tally carefully: the sending party is always in
agreed (reconciled 2026-08-24). The
example here previously omitted it. A party agrees by proposing. let_2 came from: "pty_acme", so
pty_acme sits in agreed and every other required negotiator sits in awaiting. Unanimity is
countable only if the proposer is counted, so never treat agreed as “everyone except the sender”.kind and a sequence. kind is sent for a round somebody committed here,
and imported for correspondence seeded from a mail thread that happened before the room existed. An
imported letter always has asks: [] and decisions: [], counts toward nothing, and cannot be created
through the API. See List letters.
List the live asks
endpoint
Scopes:
contracts:readstring
open (default) · all · undecidedstanding words, proposed words, why, per-party tally, and decided_by_me drawn from your own
unsent round. undecided is the working list: the asks waiting on you specifically.
Stage a decision
endpoint
Scopes:
decisions:write · If-Match requiredstring
The ask being answered. Use this when somebody has already raised the question.
string
The clause you want to change, when nobody has raised a question about it yet. Only valid with
type: "counter". Mutually exclusive with ask_id.string
required
accept · counter · declinestring
Required on a
counter: the words you propose instead.string
Required on
counter and decline. A counter without a reason is a fight.Request: answering an existing question
201 Created
Raising a question nobody has raised yet
The first counter on a fresh contract has no ask to answer. Somebody sent you a draft, and you want to change clause nine. Name the clause instead of an ask, and one gets created for you.Request: opening a new question on a clause
201 Created
- Send one or the other, never both. Both, or neither, returns
400 invalid_request. block_idonly works withtype: "counter". There is nothing to accept or decline yet, soacceptordeclinewith ablock_idreturns400 invalid_request.whyis still required. A proposal with no reason attached is the thing this product exists not to send.- The response always names the ask your decision sits on, whether it was found or freshly made, plus
ask_created: truewhen it was made. You can address it later without another read.
A
decline is implemented as a counter proposing the standing words. One mechanic, not two. That
is why a decline still has a tally and can still land.words_hash. Only a landed block does. That is why you can stage a whole
round without racing the counterparty.
Apply your playbook to a whole letter
endpoint
Scopes:
decisions:write · If-Match requiredstring[]
Which questions to work through. Defaults to every open one.
string[]
Which rules to use. Defaults to every rule your org holds.
boolean
When true, returns the identical body and stages nothing.
200 OK
reason is a closed list: no_rule, below_floor, above_ceiling, already_decided,
needs_a_human, not_a_negotiator.
Read your unsent reply
endpoint
Scopes:
contracts:read200 OK
string[]
Which clauses would actually change if you committed this round right now. Computed by the same code
that decides it for real.
string[]
Which clauses would become open questions instead.
Never work this out yourself. Two implementations of “did everyone agree” is two answers to the
only question that moves words. The server computes it here so your preview and the real thing can
never disagree.
Withdraw a staged decision
endpoint
Scopes:
decisions:writeDiscard the whole round
endpoint
Scopes:
decisions:writeSet the reply note
endpoint
Scopes:
decisions:writeRequest
note on the commit
call below still sets it at Send; this sets it earlier, on the private draft, and sends nothing.
This is where the harness’s
set_reply_note lands (reconciled 2026-08-24). A staging verb with no
endpoint is a real parity break. apply_playbook was the other one, and it now has
an endpoint too. See the parity
principle for why the reading tools are a different case.Commit the round
endpoint
required
Scopes:
send · Idempotency-Key required · If-Match required · confirmed_by requiredRequest
200 OK: the counter above, committed
string[]
Blocks that reached unanimity on this Send. The only way words ever change.
string[]
Questions this round put on the table.
string[]
Decisions invalidated because a concurrent letter moved that block. Re-decide those, and only those.
re_asked is empty here because nothing happened at the same time. It fills only when a letter from
the other side moved a clause while your round was staged, which is the 409 words_changed story
below.
The second half of the cycle, where words actually move
Request: the other side accepts
200 OK, and now the round is committed
blk_9 now reads “nine (9) months”, the contract is agreed,
and signing can start.
If your If-Match hash was stale you get 409 words_changed carrying the current hash and a
non-empty re_asked list. Refetch the contract, re-decide the named asks, retry. If you sent no
If-Match at all, you get 428 if_match_required instead, which is a different problem with a
different fix.
Withdraw a sent ask
endpoint
Scopes:
decisions:writeask_voided_by_counter.
Playbooks
endpoint
Scopes:
contracts:read?topic=.
200 OK
object | null
The worst position that is still inside the rule.
null on a rule that is prose-only.object | null
The best position the rule asks for.
null on a prose-only rule.string
org_settings · template · inherited: where the rule came from, so a reader can tell a
deliberate floor from an inherited default.Read-only, and that is the whole shape (reconciled 2026-08-24). The endpoint was public in the
draft with no documented response. There is no
POST, PATCH or DELETE: teaching a floor is org
settings, and a rule a caller could write is a rule the API would have to arbitrate.Your playbook is your side’s only. It is never returned to the counterparty, and the MCP server
deliberately does not expose it as a raw tool. An agent gets readings from risk_scan, not
the floors as data.What does not exist
No direct text write
PUT /contracts/{id}/blocks/{id} does not exist and will not. The one call that touches block text
without an ask (blocks/{id}/confirm)
only settles a span to a value the parse proposed, and only before the first Send.No decision.staged webhook
A staged decision is private to your side and must not leave it.
No comments API
Internal notes are a separate party-private layer, not designed at v1.
No approval routing
An approver acts in Needs attention, not over HTTP.