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_url and poll approval_status (or GET /v1/approvals/{id}/) until the status is terminal. approval_list shows everything still pending.
  • Cancel a stale request. If the payment is no longer needed, approval_cancel(approval_id=...) withdraws a still-pending request. Cancel fires no webhook, and cancelling an already-decided request is a 409.

Next rung

Watch the pricing page — the fourth primitive: reacting to the world changing, not to a clock or a person.