Skip to main content
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 maps them.
One endpoint answers the only question that matters on a Monday morning: what is waiting on me?

List what needs attention

endpoint
Scopes: contracts:read. Contracts-Acting-Person is required.
Returns everything waiting on the acting person, grouped by relationship. Cursor-paginated with starting_after and limit, ordered by urgency.
200 OK

Four kinds of row

Not every row is a letter. Somebody has to sign, somebody signed and then the words moved, somebody on your own side has to say yes before a position goes out. Every row carries a kind, so a client can branch on it. Common on every row: id, kind, contract, title, state, since, why_you.
string
required
A stable atn_ id. It is what starting_after takes, and it is what you dedupe on across a poll and a webhook-driven refresh.
string
required
One of letter, sign_wait, resign, approval. This is the field to branch on.
string | null
required
Nullable. It is null on every kind except letter.
string
required
When this started waiting on you.
string
required
Why this row is yours, in one sentence. Not a role name. A reason, written for a person to read, so never the thing your code branches on.
integer
On letter rows. How many things this round asks of you.
boolean
On letter rows. Whether you have something staged on this contract and have not sent it. Private to your seat.

Ordering is urgency, not recency

  1. Groups are ordered by their most urgent row. A group is never split across a page.
  2. Within a group: resign first, then approval, then sign_wait, then letter.
  3. Within one kind, the oldest since comes first, because the thing that has been waiting longest is the most urgent.
Newest first would put a letter that arrived this morning above a re-sign row that has been blocking a signature for a week. The week-old row is the one holding the deal up, so it sorts to the top.

Pagination

limit counts rows, never relationships. A group is never split, so a page can come back with slightly more rows than you asked for in order to finish the last group. starting_after takes a row id, the atn_ one. has_more and next_cursor are always present.

What is waiting on them

waiting_on_them is a separate tail of the response, outside data. It holds the contracts where the next move is theirs, each one carrying relationship, counterparty, contract and since. Keeping it out of data is the point: the count at the top of your inbox then stays honest about what is actually yours to do.

The acting person is required here

Contracts-Acting-Person is required on this read, and this is the one read where that is true. An org key without it gets 400 acting_person_required. Not an empty list, and not a merged org-wide queue.
This feed belongs to one person, and its unsent field is private to that seat, so merging everybody’s queue would push one person’s private drafts sideways across the org. See Errors for the envelope.

It is read-only

There is no POST /attention, no dismiss, no snooze, and no mark-as-read.
A row leaves the list when the thing it was waiting on happens. You send a reply, you sign, the contract executes or is cancelled. Attention is a computed view of state, not a mutable inbox, so it can never disagree with the contracts it describes. Rows are cleared by acting on the contract, so the verb you want lives on the contract: A forwarded mail thread also shows up in this feed when ingress.detected fires, and that item has two verbs of its own: POST /ingress/{id}/accept and POST /ingress/{id}/reject.

Approvals appear here, but you approve in the app

An approval row tells an approver what is waiting on them, and that is all the API does. Approval routing is explicitly not a v1 API surface, so GET /attention shows an approver their rows and there is no endpoint to approve through.

Building an inbox

Poll GET /attention on login, then refresh it from webhooks rather than on a timer. Seven events move this feed: letter.received · reply.sent · contract.agreed · words.changed · contract.executed · contract.cancelled · ingress.detected
The last five mint and clear the kinds that are not letters. Refresh only on letters and a re-sign row turns up late, while a sign-wait row never clears at all.
unsent is the field that makes a letter row honest. A contract with a reply staged and nothing sent is waiting on you, not on them, and a Monday inbox that hides that is lying to its reader.