The contract
Three outcomes, not two
A two-outcome contract forces the third to disguise itself as another, and the disguised one is always the one that takes longest to understand. Branch once on status.
Last updated
| status | HTTP | meaning |
|---|---|---|
| done | 200 | Done. Carries the output and the idempotencyKey. |
| pending_approval | 202 | Waits for a human signature and may complete hours later. A webhook tells you when it resolves. Do not retry. |
| refused | 4xx | Not done. reason is one of a closed list, and detail carries what helps: for stale, a message saying what to do. |
The shape of a reply
{ "status": "done", "output": { … }, "idempotencyKey": "…" }
{ "status": "pending_approval", "proposalId": "…", "idempotencyKey": "…" }
{ "status": "refused", "reason": "stale", "detail": { "why": "client_ambiguous", "message": "…" }, "idempotencyKey": "…" }Reads: two outcomes
A read changes nothing, so it never waits for a signature: done (200) with the output, or refused (4xx) with a named reason. It carries no idempotencyKey either.
{ "status": "done", "output": { … } }
{ "status": "refused", "reason": "role_forbidden", "detail": { "message": "…" } }