Skip to main content
Some spend should not be a machine’s call. An approval threshold does not refuse the action; it parks it until a person decides.

What your agent sees

When a rule holds an action, the authorize call returns 202 and the intent is in state requires_approval:
202 Accepted
There is no cosignature. Nothing has been authorized yet, and nothing mints one later either: approving moves the intent to pending and returns it, without a signature. The budget is reserved while the intent waits, so a held action cannot be quietly outspent by the next one. headroom was measured before that reservation, which is why it reads $4,800 rather than what is left now.

How your agent should wait

Do not busy-poll, and do not treat a hold as a failure to retry around. Retrying with a new idempotency key creates a second held intent and asks a person the same question twice.

Webhook

Subscribe to action.requires_approval and approval.resolved. This is the one to use in production.

Poll the intent

GET /control/v1/agent/actions/{id} until state leaves requires_approval. Fine for a script.

What the person sees

An approver who only sees “approve $2,400?” is rubber-stamping. So there is an endpoint that answers what approving would actually do, before anyone clicks:
200 OK
session is the budget and runtime is the agent, in the wire names this endpoint has always used. Three figures carry the decision. Budget is already reserved while an intent is held, so approving costs no further headroom. returns_on_deny_cents is what denying gives back. And open_holds counts the held intents competing for the same budget, including this one, which is how an approver sees that three pending approvals total more than the budget can cover. executed is always false here. The endpoint answers what approving would do, and Kordio never executes anything, so it has nothing else to report. GET .../payment_intents/{id}/impact is the same shape, plus the spend token the payment cites, if any.

Resolving it

Both calls require the approve capability, and both record resolved_by_sub, which is who decided, and emit approval.resolved.

Actions and payments resolve the same way

Approving grants authority. It never moves money: an approved payment is still waiting on your agent to execute it and say what happened.
A denial is not a rate limit. Do not re-ask with a fresh idempotency key. If your agent genuinely needs a different action, that is a different action: new amount, new resource, new key.
Resolving an intent that is not in requires_approval is a 422, so a double-click cannot double-execute.

Who can approve

Approving is a capability, not a job title. approve is separate from manage_policy, so the person who authorises a payment need not be the person who can widen the rule that held it. Keep those apart. Segregation of duties is most of what an auditor is looking for here. What the product does not do for you: it will not route a hold to a named person, a rota, or a group, and it has no quorum. Anyone holding approve in the workspace can resolve any hold in it, one call resolves it, and resolved_by_sub records who. There is also no escalation and no expiry, so a hold that nobody answers waits, and its reservation waits with it.

Next steps

Webhooks

Wait on approval.resolved instead of polling.

Verifying an authorization

Why approval.resolved is not itself permission.