Set up
Approvals
When a policy holds a purchase for a person, how they approve or deny it, how the agent finds out, and how to make sure a model can never approve its own spend.
When an approval: rule matches, the purchase stops before any money moves. The agent gets APPROVAL_REQUIRED with an approvalId, and the gateway records an approval request:
{
"id": "apr_7c1e…",
"workspaceId": "wsp_…",
"agentId": "invoice-bot",
"walletId": "wal_91c2…",
"capabilityId": "cap_7f3a…",
"amountMicros": "12000000",
"currency": "USD",
"rule": "price > 5",
"status": "pending",
"createdAt": "…"
}The flow
The agent is refused
Nothing is charged. With the SDK, invoke throws ZanoraPolicyRejection with code: "APPROVAL_REQUIRED" and approvalId. With MCP, the tool result says the same in plain language.
A person decides
In the console: Buyer → Approvals shows pending requests with the amount, capability, the agent that made the call (its name, with its wallet id underneath) and the rule that held it. Click approve or deny. Below it, History is the audit log of every decided request, newest first (the latest 50): when it was decided and requested, the agent, capability, rule and amount, the decision, who decided (you or another account in the console, or a prompt in the agent's own client) and the outcome (purchased and when, not used yet, or not purchased). With the API:
curl -s "https://api.zanora.dev/v1/approvals?status=pending" -H "x-api-key: $WKEY"
curl -s -X POST https://api.zanora.dev/v1/approvals/apr_7c1e…/approve -H "x-api-key: $WKEY" \
-H 'content-type: application/json' -d '{"resolvedBy":"dana@northwind.example"}'The agent retries once
An approval covers one purchase by that wallet of that capability, at up to the approved amount. It's used up by the next matching purchase, and a third call is held again. One approval covers the purchase however many of your approval rules it matches, and it's only used up when the purchase is allowed: if an allow policy refuses the call, the approval stays unused. zanora_approval_status reports a used approval as consumed, which means approved and spent, not refused. If two purchases race for the same approval, exactly one goes through; the other is held under a new approval id. Likewise, if two people answer the same request at once, the first answer stands and the second is refused with approval … already approved (or denied), so one click can't overwrite another. The agent learns the decision with zanora_approval_status (MCP), or by polling GET /v1/approvals.
Approving inside an MCP client
If the client supports MCP elicitation, @zanora/mcp asks the user directly at the moment of the purchase, and retries once if they approve:
Approve a $12.00 USD purchase?
Buying Invoice OCR
Capability cap_7f3a…
Wallet wal_91c2…
Held by price > 5
[ Approve — spend $12.00 USD ] [ Deny — do not buy this ]- The prompt is built from the gateway's approval record, never from the tool call. A model that assembled the purchase can't write the text the person approves.
- Deny returns
APPROVAL_DENIED, and the agent is told not to retry or ask again. - Dismissing the prompt leaves the request pending. Silence never counts as approval.
- Approve retries the purchase once. If that retry is refused anyway (the price rose past what was approved, or another rule stopped it), the result says so: an
approvedblock naming the approval that was given, plus the newapprovalId(APPROVAL_REQUIRED) or the refusal (POLICY_DENIED). The person isn't asked a second time in a loop. - Clients without elicitation are never prompted. They get the approval id and stop.
Keeping approval with people
Recording an approval is an authenticated call, so it only succeeds if the agent's key has the approvals:write scope. A workspace root key has it. Two ways to stop a model approving its own spend:
| Setting | Effect |
|---|---|
Give the agent a key without approvals:write | the agent can't record an approval, so the MCP server doesn't prompt you at all: the refusal says the purchase is waiting in Buyer → Approvals. Enforced by the platform. Recommended. How to create one |
ZANORA_APPROVAL_ELICITATION=off | the MCP server never shows the prompt. Enforced only by that process's configuration |
Create an agent key that can't approve
In the console: Settings → API keys, choose Agent key (can buy, can't approve), give it a label, and click Generate new key. The key is shown once, so copy or download it. In the key list, its Access reads agent · can't approve. Full access is the other choice: it can approve, like your root key, which is right for your own scripts but not for an agent.
With the API: a key's scopes are fixed when it's created, and a new key can only have scopes the key that creates it already holds. So you create the narrower key from a key you already have.
Have a workspace key to create it with
Use your workspace's root key: it was shown once when you confirmed your email. If you no longer have it, create a Full access key in the console under Settings → API keys.
export ROOT_KEY='zk.akey_….…'Create the agent key
curl -s -X POST https://api.zanora.dev/v1/auth/keys -H "x-api-key: $ROOT_KEY" \
-H 'content-type: application/json' \
-d '{"label":"invoice-bot","scopes":["discovery:read","capabilities:read","wallets:read","receipts:read","approvals:read","ratings:write","rail:read","funding:request"]}'
# → { "key": { "id": "akey_…", "label": "invoice-bot", "scopes": [ … ], "status": "active" },
# "token": "zk.akey_….…" }token is the new key, shown once. These scopes are the same set the console's agent key gets. They cover everything the MCP server's buyer tools do: search, look up a capability, buy, read balances, the ledger, purchases and receipts, rate a seller, list deposit addresses, ask you for money with a payment link (funding:request), and check an approval's status. They leave out approvals:write, so the agent can see that an approval is pending but can't decide it. To make the key expire, add "expiresInSeconds": 2592000 (30 days).
Either way, give it to the agent: put the key where the agent reads it, ZANORA_API_KEY in the MCP server's config or apiKey in the SDK. To check what it can do:
curl -s https://api.zanora.dev/v1/whoami -H "x-api-key: zk.akey_….…" # "scopes" has no approvals:writeWhen a purchase needs approval, the agent isn't prompted to ask you in chat (its key couldn't record your answer). It tells you what it wants to buy and that it's waiting in Buyer → Approvals, where you approve it.
The new key appears under Settings → API keys, where you can revoke it. Keep the root key for yourself; it can approve. See Accounts and API keys for how scopes and delegation work.