Approvals
Human-in-the-loop, for the calls you do not want an agent making unattended.
What a pause looks like
Some tools are approval-gated. When the model calls one, the run does not execute it — it yields,
persists as awaiting_approval, and your stream ends on a needs_approval frame:
event: needs_approval
data: {"runId":"01936c2f-…","approvalId":"7c41…","toolName":"send_email",
"argsJson":"{\"to\":\"…\"}","approvableScopes":["run","session"],
"expiresAt":"2026-09-02T09:12:44Z"}argsJson is the model's own arguments, so you can show a human exactly what is about to
happen. Render it as text — it is model output, not your data.
Resuming
curl -N -X POST https://api.askamy.co.za/v1/runs/01936c2f-…/approvals/7c41… \
-H "X-Api-Key: $AMY_API_KEY" -H "Content-Type: application/json" \
-d '{"decision": "approve_once"}'POST /v1/runs/{runId}/approvals/{approvalId}. The continuation
streams on that response, with the same event contract as the original run. So a paused run is not
a separate integration: it is the same stream, resumed on a second call. The body can carry four
fields:
{
"decision": "approve_for_run",
"note": "Checked the recipient list with finance.",
"mcpAuth": [ { "serverSlug": "crm", "token": "…" } ],
"mcpServers": [ … ]
}decision— required; one of the values below.note— optional free text from the approver, at most 2,000 characters. It is kept with the decision, and appears as a user turn when the conversation is read back.mcpAuth— optional; a token per MCP server, as on a start call (at most 16, each token at most 8,192 characters). Send it again on resume whenever the run uses a server that needs a token. Tokens are never stored, so the continuation does not have the one you sent when the run started. A registered server that needs a token and gets none is left out of the continuation — its tools are simply not offered, and the run carries on without them — so a run that depends on that server will not finish what it started. See Tools and MCP.mcpServers— optional; the inline servers the run was started with, declared again (at most 16). They must be exactly the ones the run started with: a different, added or omitted server is refused withinline_mcp_mismatch. A run that had none sends none.
Decisions
The decision field takes one of:
approve_once— allow this one call.approveis a back-compatible alias.approve_for_run— allow it for the rest of this run.approve_for_session— allow it for the rest of the conversation. The run needs aconversationId, or this is a400.approve_always— allow it for this end user from now on, across every future run. It binds to the run's ownendUserRef, so a run started without one cannot take this decision (400), and the grant applies to that end user alone — never to the rest of the organisation. There is no/v1call to take it back, so offer it only where you mean it.deny— refuse; the run continues and the model is told.
Scopes
approve_once and deny are always available. The approvableScopes list on the pause — values run, session and always — names the wider standing grants that this particular pause will
accept, for the rest of the run, the conversation, or always. Offer only what the frame lists; a
wider decision it does not list is a 400 naming decision, and the list
exists precisely so you do not have to guess. session appears only when the tool
allows it and the run has a conversation; always only when the tool allows a
permanent grant and the run has an end user to bind it to.
What a resume can answer
- 200 — the decision was taken and the continuation streams.
- 400 — a
decisionthat is not one of the values above, a scope the pause does not admit, an over-longnote, or a malformedmcpAuth/mcpServers. - 404 — no such run or approval for you, including a run another API client started.
- 409 — the approval was already decided, the run is no longer waiting, or the pause has expired.
- 403 — a team run's assignment approval, decided with a key that may not decide it. Team runs are a Console feature; if you do not use them, ignore this case.
- 202 — a team run's assignment approval was taken. Team runs are a Console feature; if you use them, nothing streams for this answer and the team run carries on in the background.
Two answers arrive in-band instead, when the state changes between the check and the stream: approval_not_pending and inline_mcp_mismatch. See Errors.
Expiry
Every pause carries an expiresAt. After it, the pause can no longer be decided and
the run ends as expired — so a run left waiting on a human who never came back does not
sit open forever. Show the deadline; do not assume a pause is still live because you have not been told
otherwise.
If you submitted the run
A submitted (non-streaming) run pauses the same way. You will not see the frame — read GET /v1/runs/{runId}, whose pendingApprovals carries the same
information (approvalId, toolName, argsJson, approvableScopes, expiresAt), then resume with the call above. The continuation is a stream on that
call.
Cancelling does not stop a run that is waiting on an approval — the cancel call answers cancelled: false. Decide the pause; left undecided, it expires.