# 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:

```json
{
  "id": "apr_7c1e…",
  "workspaceId": "wsp_…",
  "agentId": "invoice-bot",
  "walletId": "wal_91c2…",
  "capabilityId": "cap_7f3a…",
  "amountMinor": "1200",
  "currency": "USD",
  "rule": "price > 5",
  "status": "pending",
  "createdAt": "…"
}
```

## The flow

### Step 1: 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.

### Step 2: A person decides

In the console: **Buyer → Approvals** shows pending requests with the amount, capability, wallet and the rule that held it. Click approve or deny. With the API:

```bash
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"}'
```

### Step 3: 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. 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:

```text
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.
- 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 gateway refuses to record the answer (`APPROVAL_NOT_PERMITTED`). Enforced by the platform. **Recommended.** |
| `ZANORA_APPROVAL_ELICITATION=off` | the MCP server never shows the prompt. Enforced only by that process's configuration |

```bash
# An agent key that can buy and read, but not approve
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"]}'
```
