Ask before sending money
Some actions an agent should never take alone. Paying a supplier, deleting a production database, sending a contract — these want a human in the loop before they happen. An approval is that gate: the agent describes what it wants to do, a named person approves or rejects it on a hosted page, and the agent only proceeds on a yes. This rung introduces the third primitive, the approver experience, and the decision webhook the agent waits on.
The story
A payments agent has reconciled a supplier invoice and is ready to pay
it — but company policy says a human signs off on any payment over a threshold.
So instead of paying, the agent requests an approval from Dana, carries the
invoice id in the request's metadata, and waits. Dana gets an email, reviews
the details on a hosted page, and approves. The decision webhook lands, the agent
reads the echoed invoice id, and pays.
Solve it
The Approvals view lists every request and its state. While Dana's decision
is outstanding, the request sits as pending with its action summary, the
approver, and the expires_at deadline — so anyone on the Team can see what the
agent is blocked on and who it is waiting for.
POST /v1/approvals/ with the approver's email, a human-readable
action_summary and action_details, a timeout (an ISO-8601 duration, after
which the request expires), a callback_url for the decision webhook, and
metadata to carry your own reference — here, the invoice id — through to the
decision.
curl https://aigears.example.com/v1/approvals/ \
-H "Authorization: Bearer aigears_live_..." \
-H "Idempotency-Key: pay-invoice-INV-2291" \
-H "Content-Type: application/json" \
-d '{
"approver_email": "[email protected]",
"action_summary": "Pay supplier invoice INV-2291",
"action_details": "€4,200 to Acme Supplies, net-30, due 2026-07-20.",
"timeout": "P1D",
"callback_url": "https://hooks.example.com/aigears",
"metadata": {"invoice_id": "INV-2291"}
}'
The runnable version — request, poll, list, cancel — is
examples/python/approvals_agent.py
and
examples/typescript/approvalsAgent.ts:
approval = request_approval(
client,
approver_email="[email protected]",
action_summary="Pay supplier invoice INV-2291",
action_details="€4,200 to Acme Supplies, net-30.",
callback_url=callback_url,
timeout="P1D",
metadata={"invoice_id": "INV-2291"},
)
"Pay invoice INV-2291, but get Dana's approval first."
approval_request(
approver_email="[email protected]",
action_summary="Pay supplier invoice INV-2291",
action_details="€4,200 to Acme Supplies, net-30, due 2026-07-20.",
timeout="P1D",
callback_url="https://hooks.example.com/aigears",
metadata={"invoice_id": "INV-2291"},
)
→ { "id": "01J9Z8XKQR...", "status": "pending",
"expires_at": "2026-07-04T09:00:00Z" }
The agent now waits — either on the decision webhook (push) or by polling
approval_status(approval_id=...) until the status leaves pending.
What Dana sees
Dana gets an email with a link to a hosted approval page on our own domain. That
link is a capability URL — an opaque, single-use token that is the
authorisation to decide; it needs no separate login and is consumed the moment
Dana acts, so the link can't be replayed. The page shows the action_summary and
action_details exactly as the agent supplied them, clearly labelled as coming
from the agent (never presented as our own words), and offers Approve and
Reject with an optional comment.
Acting on the decision
When Dana decides, we POST a terminal event to the callback_url — one of
approval.approved, approval.rejected, or approval.expired (if the timeout
elapses with no decision). The metadata you sent is echoed straight back, so
the agent knows exactly which invoice the decision applies to:
{
"event_type": "approval.approved",
"data": {
"approval_id": "01J9Z8XKQR...",
"decision": "approved",
"comment": "Confirmed against the PO — go ahead.",
"metadata": {"invoice_id": "INV-2291"}
}
}
On approval.approved the agent pays invoice INV-2291; on approval.rejected
or approval.expired it stands down and does not. For how to receive and verify
the webhook, see Receiving webhooks.
Variations
- Poll instead of push. If you can't host a receiver, skip
callback_urland pollapproval_status(orGET /v1/approvals/{id}/) until the status is terminal.approval_listshows everything stillpending. - Cancel a stale request. If the payment is no longer needed,
approval_cancel(approval_id=...)withdraws a still-pendingrequest. Cancel fires no webhook, and cancelling an already-decided request is a409.
Next rung
Watch the pricing page — the fourth primitive: reacting to the world changing, not to a clock or a person.