Recipient allowlist API
This page is the per-operation reference for two recipient-scoped resources that gate outbound email:
- Recipient allowlist — an opt-in, per-mailbox containment list. When a
mailbox is in
allowlistmode, the API accepts sends only to recipients on that mailbox's list. It is a hard boundary: an agent (or a leaked key) cannot email an address that is not on the list. The blocked-attempts log records every off-list send the gate rejected. - Verified recipients — the sandbox-tier confirmation list. On a Sandbox account, a new send can only go to a recipient who has clicked a confirmation link (details below).
Both compose with the account-wide do-not-contact (suppression) list, which always runs first: a recipient on both the suppression list and the allowlist is still rejected. See Suppressions for that resource and Recipient allowlist guide for the conceptual walkthrough, thread-reply bypass, and migration workflow.
The send-time rejection codes (RECIPIENT_NOT_ON_ALLOWLIST,
RECIPIENT_AGENT_CONTAINED, RECIPIENT_SUPPRESSED) fire on the send
endpoints, not on the CRUD endpoints below — see the
send-gate decision tree and
Messages API. The full error catalog with remediation
lives at /agents/errors; the machine-readable spec for every
operation here is /docs/openapi.json.
Path params accept a name or a UUID. Every route whose URL contains
:mailboxId accepts either the mailbox's UUID or its name; unknown names return
404 NOT_FOUND in the same shape as unknown UUIDs.
Operation classification
Every operation below carries one classification tag:
| Tag | Meaning |
|---|---|
read-only | No state change; safe to retry. |
mutating | Changes stored configuration. |
send-triggering | Dispatches an outbound email as a side effect. |
human-review-possible | May route to human review before completing. |
secret-revealing | Returns a credential or secret in the response. |
No operation on this page is human-review-possible or secret-revealing.
Enable or disable allowlist mode
PATCH /v1/mailboxes/:mailboxId
Classification: mutating
Auth: Admin key or dashboard session. Agent keys receive
403 INSUFFICIENT_SCOPE, so a contained agent cannot un-contain itself.
The allowlist is enabled by flipping the mailbox's recipient_policy_mode. This
is one field on the mailbox-update endpoint — see
Mailboxes API for the full field set (thread-reply
bypass, agent-send containment, and the other mailbox controls). Only the
allowlist-relevant fields are described here.
Request fields (allowlist-relevant):
| Field | Type | Notes |
|---|---|---|
recipient_policy_mode | "blocklist" | "allowlist" | Flipping is idempotent — a PATCH with the current value is a no-op (no audit row, no webhook). |
force_empty | boolean | Required to flip to allowlist while the list is empty. Without it the server returns 400 ALLOWLIST_EMPTY. |
Errors:
| Code | HTTP | When |
|---|---|---|
ALLOWLIST_EMPTY | 400 | Flip to allowlist on an empty list without force_empty: true. A concurrent delete landing between the pre-check and commit can also return this. |
INSUFFICIENT_SCOPE | 403 | Agent-role key. |
Populate the list before flipping the mode, or pass force_empty: true only
if you specifically want to lock the mailbox down while you populate it.
List allowlist entries
GET /v1/mailboxes/:mailboxId/allowlist
Classification: read-only
Auth: Admin key, dashboard session, or an agent key bound to the mailbox. An
agent bound to a different mailbox receives 403 MAILBOX_ACCESS_DENIED. Agents
may list so a runtime can see what it is allowed to email.
Query params:
| Param | Type | Notes |
|---|---|---|
limit | integer, 1–500 | Default 500. |
cursor | string | Opaque base64 (created_at, id) tuple from next_cursor. |
all | boolean | Bypasses the default page cap; hard-capped at 10,000 server-side. |
Response 200:
{
"allowlist": [
{
"email": "[email protected]",
"mailbox_id": "<uuid>",
"created_at": "2026-04-18T12:00:00.000Z",
"added_by_actor_type": "admin",
"added_by_actor_id": "<api-key-uuid>",
"pattern_type": "email"
}
],
"next_cursor": null,
"always_allowed": [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
]
}Rows are ordered newest-first. pattern_type is "email" or "domain" (see
Entry formats). When more rows remain, next_cursor is a
non-null token to pass back as cursor.
always_allowed lists the four ReplyLayer test addresses. Agents can send to
them, and to their +label forms, without an entry, because they never reach
anyone (see the simulator guide). The array is the same
on every page. It is not a list of rows: the addresses can't be removed, and they
don't count toward the non-empty check for allowlist mode. Your do-not-contact
list still applies to them.
Errors: 400 INVALID_CURSOR, 403 MAILBOX_ACCESS_DENIED, 404 NOT_FOUND
(mailbox missing or soft-deleted). Remediation: /agents/errors.
Add a recipient
POST /v1/mailboxes/:mailboxId/allowlist
Classification: mutating
Auth: Admin key or dashboard session. Agent keys receive
403 INSUFFICIENT_SCOPE — allowlist mode only holds if agents cannot edit the
list they are contained by.
Request body:
{ "email": "[email protected]" }The email field accepts an exact address ([email protected]) or a bare-domain
pattern (@corp.com). See Entry formats.
Response 200:
{
"email": "[email protected]",
"mailbox_id": "<uuid>",
"created_at": "2026-04-18T12:00:00.000Z",
"already_existed": false,
"added_by_actor_type": "admin",
"added_by_actor_id": "<api-key-uuid>",
"pattern_type": "email"
}Idempotent: a repeat add returns already_existed: true with
created_at: null, no second audit row, and no second webhook. The server
normalizes every address (trim + lowercase) before storing, so [email protected]
and [email protected] are the same entry.
Errors:
| Code | HTTP | When |
|---|---|---|
INVALID_EMAIL | 400 | Malformed address or domain pattern (message: "Invalid email or domain pattern"). |
INSUFFICIENT_SCOPE | 403 | Agent-role key. |
NOT_FOUND | 404 | Mailbox missing or soft-deleted. |
RATE_LIMITED | 429 | Hourly add budget exceeded — see Rate limits. |
Webhook: a newly-inserted row fires recipient_allowlist.added.
Bulk add recipients
POST /v1/mailboxes/:mailboxId/allowlist/bulk
Classification: mutating
Auth: Admin key or dashboard session. Agent keys receive
403 INSUFFICIENT_SCOPE.
Request body (up to 1,000 addresses per request):
{ "emails": ["[email protected]", "@partner.com", "[email protected]"] }The input array is normalized and deduped before insert. Partial success is the norm — each address lands in exactly one bucket.
Response 200:
{
"added": [
{ "email": "[email protected]", "created_at": "...", "pattern_type": "email" },
{ "email": "@partner.com", "created_at": "...", "pattern_type": "domain" }
],
"already_existed": ["[email protected]"],
"invalid": [{ "email": "bad", "reason": "invalid_format" }],
"counts": { "added": 2, "already_existed": 1, "invalid": 1, "total": 4 }
}counts.total is the deduped input size, not the raw array length. Each newly
inserted row fires one recipient_allowlist.added webhook.
Errors: 403 INSUFFICIENT_SCOPE, 404 NOT_FOUND, 429 RATE_LIMITED — a
1,000-address request consumes 1,000 against the hourly budget. Remediation:
/agents/errors.
Remove a recipient
DELETE /v1/mailboxes/:mailboxId/allowlist/:email
Classification: mutating
Auth: Admin key or dashboard session. Agent keys receive
403 INSUFFICIENT_SCOPE — only a human can lift a containment entry.
:email is the exact stored string. A domain entry is deleted URL-encoded:
@corp.com → %40corp.com. The delete trusts the caller's string, so an entry
always removes cleanly even if a future validator would reject the exact form.
Query params:
| Param | Type | Notes |
|---|---|---|
force_empty | boolean | Acknowledges deleting the last entry while the mailbox is in allowlist mode. |
Response 200:
{
"status": "removed",
"email": "[email protected]",
"mailbox_id": "<uuid>",
"created_at": "2026-04-18T12:00:00.000Z",
"pattern_type": "email"
}Errors:
| Code | HTTP | When |
|---|---|---|
INSUFFICIENT_SCOPE | 403 | Agent-role key. |
NOT_FOUND | 404 | Mailbox or entry missing. |
ALLOWLIST_LAST_ENTRY | 409 | The delete would empty the list while the mailbox is in allowlist mode; retry with ?force_empty=true. In blocklist mode the guard does not fire. |
Webhook: fires recipient_allowlist.removed.
List blocked send attempts
GET /v1/mailboxes/:mailboxId/allowlist/blocked-attempts
Classification: read-only
Auth: Admin key, dashboard session, or an agent key bound to the mailbox
(mirrors listing the allowlist — an agent seeing its own rejection history is
legitimate self-observability). Agents bound elsewhere receive
403 MAILBOX_ACCESS_DENIED.
Every off-list send rejection is recorded append-only. This endpoint reads that log. It has two shapes: an aggregated default (collapse volume) and a raw per-attempt view (forensic drill-in). These rejections do not count toward abuse or reputation signals — the gate fires before any message row is written.
Query params:
| Param | Type | Notes |
|---|---|---|
aggregate | boolean | Default true. Set false for raw per-attempt rows. |
within_days | integer, 1–365 | Optional recency filter, e.g. 7 for "blocked this week". Applies to both shapes. |
limit | integer, 1–500 | Default 500. |
cursor | string | Opaque (created_at, id) tuple. Raw view only. |
all | boolean | Enumerate; raw view only, hard-capped at 10,000. |
Aggregated response (default), 200:
{
"attempts": [
{
"recipient": "[email protected]",
"actor_type": "agent",
"actor_id": "<api-key-uuid>",
"count": 12,
"first_attempted_at": "2026-04-19T14:23:05.211Z",
"last_attempted_at": "2026-04-19T18:47:31.004Z",
"origins": ["send", "reply"]
}
],
"next_cursor": null
}Groups by (recipient, actor_id) with a count, first/last timestamps, and the
set of send origins, ordered by most-recent attempt. next_cursor is always
null in aggregated mode. Use ?aggregate=false to enumerate exhaustively.
Raw response (?aggregate=false), 200:
{
"attempts": [
{
"attempted_at": "2026-04-19T14:23:05.211Z",
"recipient": "[email protected]",
"actor_type": "agent",
"actor_id": "<api-key-uuid>",
"origin": "send",
"draft_id": null
}
],
"next_cursor": null
}origin is send, reply, or draft_dispatch; draft_id is set only when the
rejection came from dispatching a scheduled draft.
Errors: 400 INVALID_CURSOR, 403 MAILBOX_ACCESS_DENIED, 404 NOT_FOUND.
Webhook: the gate also fires recipient_allowlist.blocked_attempt at
rejection time, deduped to at most one delivery per (account, mailbox, recipient) per 60 seconds so a runaway agent produces one alert, not hundreds.
This endpoint keeps full history regardless.
Verified recipients (sandbox tier)
On the free Sandbox you can email your own address and anyone who confirms by link, reply to people who write to you from an authenticated address, and add up to 5 people you know, yourself or through your agent. Add $5 of pay-as-you-go credit or upgrade to email anyone.
The rule applies to every send from the account, human or agent, in either mailbox
mode, at every trust level. It always admits your account's own email address, the
ReplyLayer simulator's scenario addresses (delivered@, bounced@,
complained@, [email protected]) and Mailgun's
simulator.mailgun.org addresses. A reply or thread continuation to the sender of
an inbound message is admitted on that ground only when the sender's address was
authenticated. Any other recipient must be on this list and sendable, or the send
is refused with 403 SANDBOX_RECIPIENT_NOT_VERIFIED.
A recipient becomes sendable in one of two ways. A confirmation request
validates the address and emails a link; the recipient becomes sendable when they
click it, on a tokenized browser page (there is no customer-facing confirm
endpoint). An account attestation (attest: true) validates the address and
makes it sendable at once, with no email: the account vouches that it knows the
person. A trial can attest up to 5 recipients, shared by the owner and every agent
key or app connection, and never refunded. One verified phone number funds one
trial's allowance, so a second Sandbox account on the same phone gets none of its
own for 90 days. If someone added this way reports your email as spam, every
attested recipient must confirm by link before it can be emailed again and the
account's attestations end for the trial; a report from a second person suspends
the Sandbox account.
This gate is separate from the recipient allowlist, which binds only agent sends on an allowlist-mode mailbox; a send that both apply to must pass both. See Limits for the sandbox tier model.
Add a verified recipient
POST /v1/recipients
Classification: send-triggering (dispatches a confirmation email, unless attest is true)
Auth: Admin key or dashboard session. An agent key or app connection may call
it only while agent recipient writes are on, and only when every mailbox it is
bound to is open (not in allowlist mode); otherwise 403 INSUFFICIENT_SCOPE or
403 RECIPIENT_WRITE_AGENT_CONTAINED. Sandbox tier only.
Request body:
{ "email": "[email protected]", "attest": false }Response 201 (confirmation request):
{ "id": "<uuid>", "email": "[email protected]", "status": "pending", "created_at": "..." }Response 201 (attest: true):
{ "id": "<uuid>", "email": "[email protected]", "status": "confirmed", "created_at": "...",
"attestation": "account_attested",
"complaint_notice": "If this person reports your email as spam, everyone you added without confirmation must confirm by link before you can email them again, and you can no longer add people without confirmation; a report from a second person suspends your Sandbox account." }A requested recipient stays pending until they confirm. Per-account limits: at
most 10 adds and resends per hour, and at most 5 recipients in the first 72 hours
and 25 after that (your own address does not count). Each confirmation request uses
one of the day's sends. GET /v1/auth/me reports the attestations left in
limits.sandbox_attestations_remaining.
Errors:
| Code | HTTP | When |
|---|---|---|
VALIDATION_ERROR | 400 | Malformed address after normalization. |
TIER_LIMIT | 403 | The recipient cap is reached (ramp_until during the first 72 hours), or, with attest: true, no attestations are left (attestations_remaining: 0; forfeited: true once the account's attestations were forfeited). |
CONFLICT | 409 | Already confirmed, or (for a confirmation request) already pending. |
RECIPIENT_VALIDATION_FAILED | 422 | The address failed deliverability validation. |
RECIPIENT_VALIDATION_UNAVAILABLE | 503 | The validation check was unavailable while validation is enforced. Usually transient: the response carries Retry-After: 60; retry the same call once after about a minute, and if it fails again, stop and tell the user. |
RATE_LIMITED | 429 | More than 10 adds in the hour. |
List verified recipients
GET /v1/recipients
Classification: read-only
Auth: Any account-scoped key (admin, agent, or session).
Response 200:
{
"recipients": [
{ "id": "<uuid>", "email": "[email protected]", "status": "confirmed", "created_at": "...", "confirmed_at": "...", "attestation": "account_attested" },
{ "id": "<uuid>", "email": "[email protected]", "status": "pending", "created_at": "...", "confirmed_at": null, "attestation": "confirmed" }
]
}status is confirmed, pending, or expired (the confirmation link expired
after 48 hours without a click). attestation says how the recipient became, or
will become, sendable: confirmed (they click the link) or account_attested (the
account added them). An attested recipient whose attestation was revoked (after a
spam report, or by ReplyLayer) reads expired; resend asks them to confirm by link.
Resend a confirmation email
POST /v1/recipients/:id/resend
Classification: send-triggering (re-dispatches a confirmation email)
Auth: Same as adding a recipient.
Regenerates the token and extends expiry by 48 hours. Shares the 10-per-hour add budget. A pending recipient with stale validation metadata is revalidated before the new email is sent.
Response 200: { "status": "sent" } — or { "status": "already_confirmed" }
if the recipient is already confirmed.
Errors: 404 NOT_FOUND, 422 RECIPIENT_VALIDATION_FAILED,
429 RATE_LIMITED, 503 RECIPIENT_VALIDATION_UNAVAILABLE.
Remove a verified recipient
DELETE /v1/recipients/:id
Classification: mutating
Auth: Admin key or dashboard session.
Response 200: { "status": "deleted" }
Errors: 404 NOT_FOUND.
Entry formats
Allowlist entries are exact addresses or bare-domain patterns; mix both freely in one list.
| Entry | Matches | Does not match |
|---|---|---|
[email protected] | [email protected] only | everyone else |
@corp.com | any *@corp.com | [email protected] — add @sub.corp.com explicitly |
Every allowlist read/write surface exposes a derived pattern_type of "email"
or "domain". Matching is exact and case-insensitive; sub-addresses are
distinct ([email protected] and [email protected] are separate entries).
Validation (applied server-side, shared with suppressions):
- Input is trimmed and lowercased before validation.
- Domain patterns must be multi-label ASCII:
@corp.com,@my-company.io,@sub.example.co.ukare valid. - Rejected:
@,@.com,@foo,@foo.,@.foo.com,@foo..com,@-corp.com,@_foo.com, and any non-ASCII. - Length caps: total domain ≤ 253 chars; each label ≤ 63 chars.
- Subdomain wildcards (
@*.corp.com) and IDN/punycode are not supported.
Rate limits
The allowlist add endpoints (single + bulk) share a per-account hourly budget of
5,000 entries added — it counts entries, not requests, so a 1,000-address bulk
request consumes 1,000. Every add response carries the observable budget in the
X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers;
a 429 RATE_LIMITED also returns a Retry-After header and
details.retry_after (seconds). Verified-recipient adds are capped separately at
10 per hour.
Webhook events
Four events accompany the operations above. Payload shapes and the delivery contract are documented once at /agents/webhooks — subscribe there, do not restate here.
| Event | Fires on |
|---|---|
recipient_allowlist.added | A newly-inserted allowlist entry (single or bulk). |
recipient_allowlist.removed | An allowlist delete. |
mailbox.recipient_policy_changed | A recipient_policy_mode flip (only when the mode actually changes). |
recipient_allowlist.blocked_attempt | The allowlist gate rejects a send (deduped, one per recipient per 60 s). |
These four are exempt from delivery-time PII redaction — the address is operator-authored configuration, so redacting it would defeat the event's purpose.