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 allowlist mode, 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. Sandbox accounts can only send to recipients who have clicked a confirmation link.

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:

TagMeaning
read-onlyNo state change; safe to retry.
mutatingChanges stored configuration.
send-triggeringDispatches an outbound email as a side effect.
human-review-possibleMay route to human review before completing.
secret-revealingReturns 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):

FieldTypeNotes
recipient_policy_mode"blocklist" | "allowlist"Flipping is idempotent — a PATCH with the current value is a no-op (no audit row, no webhook).
force_emptybooleanRequired to flip to allowlist while the list is empty. Without it the server returns 400 ALLOWLIST_EMPTY.

Errors:

CodeHTTPWhen
ALLOWLIST_EMPTY400Flip 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_SCOPE403Agent-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:

ParamTypeNotes
limitinteger, 1–500Default 500.
cursorstringOpaque base64 (created_at, id) tuple from next_cursor.
allbooleanBypasses 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
}

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.

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:

CodeHTTPWhen
INVALID_EMAIL400Malformed address or domain pattern (message: "Invalid email or domain pattern").
INSUFFICIENT_SCOPE403Agent-role key.
NOT_FOUND404Mailbox missing or soft-deleted.
RATE_LIMITED429Hourly 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:

ParamTypeNotes
force_emptybooleanAcknowledges 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:

CodeHTTPWhen
INSUFFICIENT_SCOPE403Agent-role key.
NOT_FOUND404Mailbox or entry missing.
ALLOWLIST_LAST_ENTRY409The 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:

ParamTypeNotes
aggregatebooleanDefault true. Set false for raw per-attempt rows.
within_daysinteger, 1–365Optional recency filter, e.g. 7 for "blocked this week". Applies to both shapes.
limitinteger, 1–500Default 500.
cursorstringOpaque (created_at, id) tuple. Raw view only.
allbooleanEnumerate; 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)

Sandbox accounts can only send to confirmed recipients. Adding a recipient validates the address and dispatches a confirmation email; the recipient becomes usable only after they click the emailed link. The click happens on a tokenized browser page, not via an API call you make — there is no customer-facing confirm endpoint. This gate is independent of the recipient allowlist; in sandbox both must pass. See Limits for the sandbox tier model.

Add a verified recipient

POST /v1/recipients

Classification: send-triggering (dispatches a confirmation email) Auth: Admin key or dashboard session. Sandbox tier only.

Request body:

{ "email": "[email protected]" }

Response 201:

{ "id": "<uuid>", "email": "[email protected]", "status": "pending", "created_at": "..." }

status stays pending until the recipient confirms. Per-account limits: at most 10 adds per hour and 25 recipients total (pending + confirmed).

Errors:

CodeHTTPWhen
VALIDATION_ERROR400Malformed address after normalization.
CONFLICT409Already pending or confirmed, or a per-account cap is hit.
RECIPIENT_VALIDATION_FAILED422The address failed deliverability validation.
RECIPIENT_VALIDATION_UNAVAILABLE503The validation check was unavailable while validation is enforced.
RATE_LIMITED429More 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": "..." },
    { "id": "<uuid>", "email": "[email protected]", "status": "pending", "created_at": "...", "confirmed_at": null }
  ]
}

status is confirmed, pending, or expired (the confirmation link expired after 48 hours without a click).

Resend a confirmation email

POST /v1/recipients/:id/resend

Classification: send-triggering (re-dispatches a confirmation email) Auth: Admin key or dashboard session.

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.

EntryMatchesDoes not match
[email protected][email protected] onlyeveryone else
@corp.comany *@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.uk are 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.

EventFires on
recipient_allowlist.addedA newly-inserted allowlist entry (single or bulk).
recipient_allowlist.removedAn allowlist delete.
mailbox.recipient_policy_changedA recipient_policy_mode flip (only when the mode actually changes).
recipient_allowlist.blocked_attemptThe 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.