# Recipient allow-lists and human approval for agent email
URL: https://emailforagents.ai/docs/sending-controls
Summary: Pause sending, restrict recipients with allow and block lists, require human approval, and cap recipients per day — with the exact precedence and reason codes.

1. [Home](https://emailforagents.ai/)
2. [Docs](https://emailforagents.ai/docs)
3. Sending controls

Docs

## Control what an inbox can send

An agent inbox is only as safe as the smallest set of things it is allowed to do. These controls decide, before any provider is contacted, whether a send happens at all, becomes a draft for a human, or is refused outright.

Last reviewed September 21, 2026 against the
implementation in this repository.

This is the reference for the controls behind
[agent email approvals](https://emailforagents.ai/product/controls). Open an inbox in the console, expand
**Sending controls**, and you are editing exactly the policy documented here. Only
a current workspace owner or administrator can change it; other authorised human members can
read it. An agent credential can read the policy but can never write one — the scope
`policies:write` is refused on a connection grant entirely.

### Three layers, one decision

Policy rows exist at three scopes and they accumulate. Evaluation walks workspace,
then project, then inbox, and any layer can deny. A narrower layer cannot loosen a wider one:
an inbox cannot re-enable sending that the workspace has paused, and it cannot allow a
recipient that the workspace blocks.

| Scope | Applies to | Typical use
| `workspace` | Every project and inbox you own | A global stop, or a company-wide blocked address
| `project` | Every inbox in that project | Holding a whole environment while something is investigated
| `inbox` | One address | The per-agent rules you edit day to day

The policy response separates the two: `send_enabled` is this inbox's own switch,
`effective_send_enabled` is the answer after every inherited layer and the inbox's
own status have been taken into account. Show the effective value in any interface a human
makes decisions in. Inherited layers are returned in `inherited` so you can explain
why an inbox is paused rather than just that it is.

### The order the rules are applied in

For each policy layer, in order, the send is evaluated against the deduplicated set of unique
To, Cc and Bcc recipients:

1. **Sending disabled?** Deny immediately with reason `send_disabled`.

2. **Any recipient on a block rule?** Deny with `blocked_recipient`.
Blocks win over allows, always.

3. **Is there a non-empty allow list?** Then every recipient must match it.
One unmatched address denies the whole message with `recipient_not_allowed`. An
empty allow list adds no restriction.

4. **Any approval rule matched?** Collect its reason and keep going; approval does
not short-circuit.

After all three layers: any collected approval reason turns the send into an approval request;
otherwise it is sent directly. A denial is a `policy_denied` response, HTTP 403,
with `retryable: false` — nothing about retrying it will change the outcome.

```
{
  "error": {
    "code": "policy_denied",
    "message": "Workspace policy blocked this send.",
    "request_id": "req_...",
    "retryable": false,
    "details": { "reason_codes": ["recipient_not_allowed"] }
  }
}
```

### Pausing outgoing mail

**Pause sending** clears `send_enabled`. New sends are denied, and the
check runs again before a queued message is handed to the provider — so a message that was
already accepted into the queue is still stopped, provided it has not been submitted yet.

Four things pause does not do:

- **It does not stop incoming mail.** Receiving and reading continue, which is
usually what you want while you investigate.

- **It cannot recall a message already submitted to the provider.** Once bytes are
accepted upstream, they are gone.

- **Resuming does not replay anything.** Canceled sends stay canceled, and a
submission whose outcome is unknown stays held for reconciliation rather than being retried.

- **It cannot reactivate an inbox paused for another reason** — an unresolved
provisioning result or an ownership problem on a custom domain. Those clear through
[domain verification](https://emailforagents.ai/docs/domains), not through this switch.

### Recipient allow and block lists

Under **Recipient and approval policy** you can list allowed or blocked domains
and exact addresses. In the console, one per line or comma-separated; over the API, four
arrays. Each list holds up to 100 entries and is normalised to lower case.

| Rule | Matches | Does not match
| Domain `example.com` | `anyone@example.com` | `anyone@mail.example.com` — a domain rule is an exact suffix match on
`@domain`, not a subdomain wildcard. List each subdomain you mean.

| Address `ops@example.com` | That address exactly, case-insensitively | Any other local part at the same domain

Domains are validated as hostnames and addresses as email addresses, so a typo is rejected when
you save rather than silently matching nothing. An allow list is the single highest-value
control on this page: while an agent is new, restrict it to the handful of addresses it
genuinely needs and a prompt-injected instruction to email someone else simply fails.

### Requiring human approval

Setting `approval_mode` to `always` routes every send from this inbox to
Approvals. The API returns `202` with
`result: "approval_required"`, an `approval_id`, a
`draft_id` and the reason codes — and no message exists yet.

Three properties worth knowing before you rely on it:

- **A draft-only credential is always reviewed**, whatever the inbox says. A
connection granted `read + draft` holds `drafts:submit` but not
`messages:send`, so its submissions become approval requests even on an inbox that
permits direct sending. Reason code: `draft_only_principal`.

- **The approval is bound to the exact content.** Both the approval and the draft
version carry a SHA-256 of the canonical payload. If the draft is edited after the approval
was created, approving it returns `version_conflict` rather than sending something
the reviewer did not read. The approval is consumed on use and cannot be replayed.

- **Approvals expire after 7 days.** An expired approval can no longer be
converted into a send; the agent must ask again. Authority is also re-checked at approval
time and again in the send job, so a credential revoked between request and approval cannot
be used to deliver the message.

Approval is not a replacement for the other controls: a direct send that clears approval is
still subject to recipient rules, the daily cap, entitlement and
[attachment scan state](https://emailforagents.ai/docs/attachments). Operator-configured approval rules that
the console cannot edit are preserved and returned in
`unmanaged_approval_rules` so they remain visible rather than silently applied.

### The daily recipient cap

`daily_recipient_limit` is optional and counts recipients, not messages, in
a UTC-day bucket. Setting it to 50 means fifty addresses across all of that inbox's messages
between one midnight UTC and the next.

The accounting is a reservation, not a counter, which is why it behaves well under failure:

| Outcome | Effect on today's bucket
| Send queued | Recipients are reserved up front.
| Provider accepted | The reservation is consumed.
| Send failed | The reservation is released and the capacity returns.
| Submission outcome unknown |
The reservation is held. It is neither spent nor returned, because releasing it
would mean overshooting the cap if the message did go out.

Hitting the cap returns `policy_denied` with the message that the daily recipient
limit has been reached. Test-mode sends never consume a live cap — the meter is only applied to
live messages. Caps at wider scopes apply as well, and billing entitlement is a separate
ceiling on top: running out of allowance returns `payment_required` or
`quota_exceeded`, both HTTP 402.

### Reading and writing the policy

The control API is for humans. It requires a human principal with a current membership, and it
does not accept an agent API key as an administrator.

```
GET /v1/projects/{project_id}/inboxes/{inbox_id}/policy
Authorization: Bearer YOUR_SESSION_CREDENTIAL
```

```
{
  "version": 3,
  "inbox_status": "active",
  "can_manage": true,
  "effective_send_enabled": true,
  "send_enabled": true,
  "approval_mode": "always",
  "allow_domains": ["example.com"],
  "allow_addresses": [],
  "block_domains": [],
  "block_addresses": ["restricted@example.com"],
  "daily_recipient_limit": 50,
  "unmanaged_approval_rules": [{ "type": "new_recipient_domain" }],
  "inherited": [{ "scope": "workspace", "version": 1, "send_enabled": true }]
}
```

Send the `version` you read back as `expected_version` when you write.
That is the whole concurrency story: if someone else saved in between, you get
`version_conflict` (HTTP 412) and nothing changes, instead of your form quietly
overwriting their block list.

```
PUT /v1/projects/{project_id}/inboxes/{inbox_id}/policy
{
  "expected_version": 3,
  "send_enabled": true,
  "approval_mode": "always",
  "allow_domains": ["example.com"],
  "allow_addresses": [],
  "block_domains": [],
  "block_addresses": ["restricted@example.com"],
  "daily_recipient_limit": 50
}
```

`expected_version: 0` is correct only when the GET returned version 0, meaning the
inbox has no policy of its own yet. Every successful write returns the new version and records
an audit entry with a before-and-after diff, naming the human who made the change.

### Reason codes

| Code | Outcome | Meaning
| `send_disabled` | Denied | Sending is paused at some layer.
| `blocked_recipient` | Denied | A recipient matched a block rule.
| `recipient_not_allowed` | Denied | A recipient was outside a non-empty allow list.
| `unsupported_policy_rule` | Denied | A rule this build cannot evaluate. Refusing is deliberate: a rule that cannot be understood must not be ignored.
| `always` | Approval | The inbox requires review for every message.
| `new_recipient_domain` | Approval | An operator-configured rule for unfamiliar recipient domains.
| `attachments` | Approval | The message carries attachments and a rule requires review for those.
| `draft_only_principal` | Approval | The credential can draft but not send.

### What this does not protect you from

- **Content.** Policy decides who may be written to and whether a human looks
first. It does not read the message. If an agent can be talked into writing something
damaging to an allowed recipient, an allow-list will not stop it.

- **Prompt injection.** Inbound mail is untrusted input. Nothing here makes an
agent resistant to instructions embedded in a message it reads; a human approval step is a
mitigation, not a fix.

- **Delivery.** Passing every control means the message was submitted, not that it
arrived. See [events and webhooks](https://emailforagents.ai/docs/events-and-webhooks).

- **Retroactive effect.** Tightening a policy changes what happens next. It does
not recall mail already accepted by the provider.

Related: [the approval and message events these produce](https://emailforagents.ai/docs/events-and-webhooks),
[the separate gate attachments must pass](https://emailforagents.ai/docs/attachments), and
[exporting the audit trail and message record](https://emailforagents.ai/docs/data-controls).

Longer form: [how the credential you choose changes who authorises a send](https://emailforagents.ai/blog/agent-email-mcp-or-api).

[Previous Quickstart](https://emailforagents.ai/docs/quickstart) [Next Events & webhooks](https://emailforagents.ai/docs/events-and-webhooks)
