# Approvals

> Draft. This page describes approvals as implemented. The production host names are not live yet.

## What waits for a person

Reading, creating a draft form and sending a test lead to the owner need no approval. Anything that changes who is notified, archives a form, deletes a lead or emails a customer waits for the site's owner or an editor to click. A key can ask, never decide.

## Ask, wait, decide

### Ask to change who is notified

*Example: Ask to change who is notified*

```
PATCH /api/forms/f_8Kq2xLm9
Authorization: Bearer <your agent key>
Content-Type: application/json

{
  "recipients": [
    "owner@example.com"
  ]
}

-> 202: a pending approval: nothing has changed yet
```

### Ask to delete a lead

*Example: Ask to delete a lead*

```
DELETE /api/submissions/kg2c4q1h7b9r3n5v8x0z6m2w4t1e
Authorization: Bearer <your agent key>

-> 202: a pending approval for the owner
```

### Check an approval

*Example: Check an approval*

```
GET /api/approvals/apv_Hq3rT8vXz1LkP9mNc2WsD7yB4eFa0uJg
Authorization: Bearer <your agent key>

-> 200: pending, used, declined or expired
```

### Approve (a signed-in person; a key is refused with 403)

*Example: Approve (a signed-in person; a key is refused with 403)*

```
POST /api/approvals/apv_Hq3rT8vXz1LkP9mNc2WsD7yB4eFa0uJg/approve
Authorization: Bearer <a signed-in person's token>
Content-Type: application/json

{
  "argsHash": "9f2c7a41d83b0e65a1c4f7d20b96e83a5d17c04be2f68a93d0715bc4e8a2f6d1"
}

-> 200: executed: the exact action ran once
```

- Asking answers `202` with `approval`: `id` (the token, shown to you once), `status: "pending"`, `expiresAt`, a `url` for the person and a `summary` of the exact action. **Nothing has changed.**
- The Agents page shows a card with the same summary, the complete list of addresses, who asked and the minutes left. Approve runs the action exactly once; Decline ends it.
- Over REST, approving sends the `argsHash` of the card the person was shown (read it from `GET /api/approvals/:id` as a signed-in person). A missing hash is 422 `validation_failed`; a hash that is not the approval's is 409 `approval_changed`.
- Addresses are trimmed, lowercased and de-duplicated before the approval is stored, so the same request written two ways is one approval.
- Only the key that asked can read the status (`pending`, `used`, `declined`, `expired`); anyone else gets 404 `approval_not_found`.
- The approval is bound to the exact arguments. If they no longer match what the person was shown, or the form or lead changed, approving answers 409 `approval_changed` and nothing runs. An approval is used once (409 `approval_used`) and expires in minutes (410 `approval_expired`).
- Asking again with the same arguments returns the same approval (`duplicate: true`); it does not pile up.

## Who can decide

A signed-in owner or editor of the site, checked again at the moment of the click (a person removed or demoted in between cannot approve). Archiving a form and deleting a lead need an owner. An API key, whatever its scope, is refused with 403 `role_not_allowed` on approve and decline.

## Limits

At most 20 approvals wait per account (the 21st is 429 `approval_limit` with a fix). Decided and expired approvals are deleted after 30 days.

## Not available yet

- Replying to a lead (needs email sending): refused up front with 503 `approval_unavailable`, nothing waits. (soon)
- Routing rules and auto-replies: refused up front with 503 `approval_unavailable`. (soon)

No MCP tool is gated: the five tools only read, create drafts or send a test to the owner. Gated changes are the REST routes above.
