# Mailboxes API reference

A mailbox is the addressable unit an agent sends from and receives into. Each one
carries its own address plus a stack of per-mailbox policies: what the scanner
enforces, how PII is delivered, whether attachments are exposed, and how sends and
inbound senders are contained. This page is the per-operation reference for the
`/v1/mailboxes` resource.

The machine-readable spec for every field and shape below is served at
[`/docs/openapi.json`](/docs/openapi.json).

## How to read this page

Each operation lists its **method + path**, **auth** (as a contract, not a
deployment fact), the request/response shape, the error codes worth knowing, and a
**classification** tag:

| Tag | Meaning |
|---|---|
| `read-only` | Returns state; changes nothing. |
| `mutating` | Changes stored mailbox state. |
| `secret-revealing` | Returns a value that is shown once and cannot be re-fetched. |

None of the mailbox operations send email or route a message into a review queue
directly, so `send-triggering` and `human-review-possible` do not appear here — but several
policy writes below *configure* how future sends are gated. Those forward links
point at the owning contract pages.

**Auth vocabulary.** "Bearer" is an API key (`admin` or mailbox-scoped `agent`
role); "session" is a dashboard cookie. Where an operation says "admin + session",
an `agent`-role key is rejected with `403 INSUFFICIENT_SCOPE`. Full remediation for
every code is in the [error catalog](/agents/errors).

**Path params accept name or UUID.** Every `:id` below accepts either the mailbox's
UUID or its name. Unknown names return `404 NOT_FOUND` in the same shape as unknown
UUIDs. A UUID-shaped token is always read as an ID, never a name.

## POST /v1/mailboxes — create a mailbox

**Classification:** `mutating` · **Auth:** admin key or session (`agent` keys →
`403 INSUFFICIENT_SCOPE`).

Creates a new mailbox and returns its sending address.

```json
{ "name": "support-bot" }
```

On a self-hosted (bring-your-own) sending domain, also send
`self_hosted_imap_folder`; the API probes the folder synchronously before insert,
and it must be unique per domain.

**Response (201):**

```json
{
  "id": "uuid",
  "name": "support-bot",
  "address": "support-bot@mail.replylayer.ai"
}
```

**Errors:** `400 INVALID_NAME` (name matches the UUID shape), `409` (name already
in use), and — self-hosted only — `400 IMAP_FOLDER_REQUIRED` /
`IMAP_FOLDER_NOT_FOUND` / `SELF_HOSTED_PROBE_FAILED`, `409 FOLDER_ALREADY_CLAIMED`.
See the [error catalog](/agents/errors).

## GET /v1/mailboxes — list mailboxes

**Classification:** `read-only` · **Auth:** any account-scoped key or session. An
`agent` key sees **only its bound mailboxes**.

Returns `{ "mailboxes": [ ... ] }`. Each entry carries the mailbox identity plus the
policy projection: `status`, `scanner_policy`, `pii_mode`, the effective
`attachment_exposure_mode` / `attachment_allowed_file_families`,
`recipient_policy_mode`, `sender_policy_mode`, `hitl_mode`, `allow_thread_replies`,
the derived `agent_send_policy` / `restricted_by` pair, an `outbound_attachments`
enablement block, and the `instruction_trust_mode` / `instruction_trust_strict_recipient`
flags. The list and single-GET projections are byte-for-byte in parity, so you can
read every mailbox's policy state in one call.

> **Session-only fields.** The attachment / outbound-attachment **consent
> bookkeeping** — `current_disclaimer_version`, the `*_accepted_at` /
> `*_accepted_version` acceptance stamps, `attachment_reauth_at`,
> `attachment_policy_version`, `image_raw_download_confirmed`,
> `current_image_risk_version`, `legacy_wildcard_active`, the legacy
> `attachment_access_enabled` flag, and the matching sub-fields of
> `outbound_attachments` — is returned **only to a dashboard session**. It drives
> the re-acceptance banner, and the routes that record acceptance require a human
> session plus re-authentication, so an API-key (Bearer) caller cannot act on it.
> These fields are therefore **omitted from every API-key response**, and the
> `outbound_attachments` block is reduced to just its `enabled` flag. An agent
> reads the effective `attachment_exposure_mode` instead.

## GET /v1/mailboxes/:id — mailbox detail

**Classification:** `read-only` · **Auth:** admin key, session, or a mailbox-bound
`agent` key. An `agent` key that is **not** bound to this mailbox sees
`404 NOT_FOUND` (the resource is resolved then masked, so existence is not leaked).

Returns the full mailbox object — the same projection as a list entry. Example
(**API-key shape**; a dashboard session additionally receives the session-only
consent fields described above):

```json
{
  "id": "uuid",
  "name": "support-bot",
  "address": "support-bot@mail.replylayer.ai",
  "status": "active",
  "scanner_policy": null,
  "pii_mode": "passthrough",
  "attachment_exposure_mode": "metadata_only",
  "attachment_allowed_file_families": [],
  "outbound_attachments": { "enabled": false },
  "default_subaddress_mode": "reply_to",
  "recipient_policy_mode": "blocklist",
  "sender_policy_mode": "blocklist",
  "instruction_trust_mode": "disabled",
  "instruction_trust_strict_recipient": true,
  "created_at": "2026-04-02T12:00:00Z"
}
```

`attachment_exposure_mode` and `attachment_allowed_file_families` are the
**effective** values from a read-time projection, not raw column values.

## PATCH /v1/mailboxes/:id — update mailbox policy

**Classification:** `mutating` · **Auth:** admin key or session only. An `agent`
key → `403 INSUFFICIENT_SCOPE`, so only a human session or an admin key can loosen a
policy.

Partial-update the mailbox's scanner policy, PII delivery mode, review-queue mode,
per-detector PII config, thread-reply behavior, and agent-send containment. **Every
body field is optional** — send only what you want to change; omitted fields are
preserved.

```jsonc
{
  "scanner_policy": {
    "language_mode": "allow_all_languages",
    "disabled_scanners": ["pii", "secrets"],
    "disabled_proxy_criteria": ["profanity"],
    "outbound_pii_policy": { "phone_number": "allow_with_warning" }
  },
  "pii_mode": "redacted",
  "hitl_mode": "all_outbound",
  "allow_thread_replies": true,
  "agent_send_policy": "restricted"
}
```

### scanner_policy

Accepts three distinct shapes:

- **An object** — shallow-merged at the top level: present keys overwrite, absent
  keys are preserved. Clear an array key by sending it as `[]` (e.g.
  `disabled_scanners: []` re-enables all scanners). A per-key `null` inside the
  object is rejected `400`.
- **`{}`** — a no-op merge; the stored policy is preserved.
- **`null`** — reset the whole policy to platform defaults. A later GET returns
  `scanner_policy: null`. This is distinct from `{}` (preserve) and from omitting
  the field entirely (column no-op).

Sub-fields:

- `language_mode` — `english_only` (default), `allow_all_languages`, or `disabled`.
  `english_only` is a **best-effort** whole-document heuristic that quarantines mail
  detected as non-English; short or mixed-language messages can be misclassified
  either way, so it is **not** a containment guarantee — the regex and LLM scanners
  are the defense against malicious content in any language.
- `disabled_scanners` — local scanners you can turn off:
  `prompt-injection`, `attachment-policy`, `mime-mismatch`, `pii`, `secrets`,
  `url-reputation`.
- `disabled_proxy_criteria` — LLM criteria you can turn off: `prompt_injection`,
  `jailbreak`, `function_call_risk`, `profanity`, `confidentiality_leak`,
  `unauthorized_liability`.
- `outbound_pii_policy` — per-type outbound PII send-safety map for `ssn`,
  `credit_card`, `phone_number`, with actions `allow`, `allow_with_warning`,
  `review`, `quarantine`, `block`. Omitted types use platform defaults (`ssn` /
  `credit_card` → `quarantine`, `phone_number` → `allow_with_warning`). Stricter
  actions are accepted on every tier; **relaxing** an action below default, newly
  disabling the `pii` scanner, or newly setting `review` requires paid entitlement
  (see [limits](/docs/limits)). An action of `review` routes matching sends into
  the review queue — see the [message lifecycle](/agents/messages).
- `outbound_review_policy.approval_note` — `optional` (default) or
  `required_for_sensitive_pii` (require an approver note before releasing SSN /
  credit-card review holds).

**Mandatory outbound criteria.** Including any of `toxicity`, `hate_speech`,
`violence`, `harassment`, `self_harm`, or `sexual_content` in
`disabled_proxy_criteria` returns `422`. These stay enforced on the shared sending
domain and are not in the inbound profile. The platform trust check
`recipient-check` is likewise not part of scanner policy.

### pii_mode

`passthrough` (default) or `redacted`. Controls how PII detected in **inbound**
bodies is delivered to the agent:

- `passthrough` — reads return `body` as a plaintext display projection, capped at
  the shared body delivery limit.
- `redacted` — reads return `body.content` with each detected PII span replaced by a
  `<TYPE>` tag (e.g. `<EMAIL_ADDRESS>`, `<PERSON>`, `<PHONE_NUMBER>`). Detected
  types are the scrub-worthy subset: `PERSON`, `EMAIL_ADDRESS`, `PHONE_NUMBER`,
  `US_SSN`, `CREDIT_CARD`, `US_BANK_NUMBER`, `US_DRIVER_LICENSE`, `US_ITIN`,
  `US_PASSPORT`, `UK_NHS`, `IBAN_CODE`, `MEDICAL_LICENSE`, `CRYPTO`. URLs, dates,
  and locations are not redacted. `redacted` requires paid entitlement — a sandbox
  account gets `403 TIER_LIMIT`. If a message's PII spans failed to persist at scan
  time, `redacted` fails closed with `body.content = null` rather than leaking.

**Advanced per-detector config** (`pii_redaction_config`) lets a paid account choose
which detectors redact and how a span renders (`replace_with_type`, `partial_mask`,
`hash_replace`). It is a paid feature that only takes effect while `pii_mode` is
`redacted`; `null` means "platform default — every detector redacts with a `<TYPE>`
placeholder". On a downgrade to a non-feature tier the saved config is retained but
ignored on reads (privacy-safe — a downgraded account never regains raw PII
delivery). Entitlement details are in [limits](/docs/limits).

### hitl_mode

`disabled` (default) or `all_outbound`. When `all_outbound`, every clean outbound
send routes to a review state instead of going on the wire, and blocks until
released. Setting `all_outbound` requires paid entitlement (`403 TIER_LIMIT`
otherwise); setting `disabled` is always allowed as a downgrade escape hatch. See
the [message lifecycle](/agents/messages) for the resulting states and the
[send-gate contract](/agents/send-gates) for where this sits in the decision tree.

### allow_thread_replies

Boolean. In an allowlist-mode mailbox this permits an outbound send to a
non-allowlisted recipient **only when** that recipient is a visible inbound
participant of the same thread (computed at send time; no standing allowlist row is
written). Inert in blocklist mode. Fail-closed: an unset value disables the bypass.
This is a send-gate control — the full decision tree lives at
[send gates](/agents/send-gates).

### agent_send_policy

`restricted` or `open` — the single front-door control for **agent-send
containment**. When containment is enforced for your mailbox, sends that originate
from an `agent`-role key are held to the recipient allowlist even while the mailbox
is otherwise in blocklist mode, so an exfiltration-style send to a brand-new
recipient is contained; human/admin/session sends keep the mailbox's native mode.
It is a derived control and is **mutually exclusive** in one request with the raw
`recipient_policy_mode` / `agent_send_containment` fields (`400` otherwise). Setting
`open` on an allowlist mailbox flips it to blocklist and clears agent containment;
it does **not** open human sends (they were never allowlist-restricted), so it
requires no consent. `confirm_open_human_sends` is deprecated and ignored (kept
for back-compat).
Responses carry the derived pair `agent_send_policy` + `restricted_by`
(`mailbox_allowlist` | `agent_containment` | `null`). The interaction with the
recipient allowlist is documented at [send gates](/agents/send-gates).

**Response (200):** the full mailbox object with the updated policy. **Errors:**
`403 TIER_LIMIT` (a requested value needs paid entitlement), `422` (invalid or
mandatory criteria, or an unknown/mis-targeted PII detector), `400` (schema-shape
rejections). Full remediation: [error catalog](/agents/errors).

## DELETE /v1/mailboxes/:id — deactivate a mailbox

**Classification:** `mutating` · **Auth:** admin key or session (`agent` keys →
`403 INSUFFICIENT_SCOPE`).

Soft-deletes the mailbox: it stops accepting new mail and its bindings are removed
from any keys. Returns `{ "status": "deleted" }`. Deleted mailboxes drop out of
every binding read path, so they never linger in a key's `mailbox_ids`.

## POST /v1/mailboxes/:id/attachment-access — inbound attachment exposure

**Classification:** `mutating` · **Auth:** admin key or session (`agent` keys →
`403 INSUFFICIENT_SCOPE`). Enabling or widening **approved raw downloads** is
**session + fresh re-auth only** — a Bearer key hits `403 REAUTH_REQUIRES_SESSION`
before any state change.

Sets the mailbox's three-mode exposure model with a versioned disclaimer
acceptance:

| `mode` | What an agent gets |
|---|---|
| `metadata_only` (default) | Filename, size, MIME, AV verdict. No content, no bytes. |
| `derived_content` | Metadata plus a 20,000-character inline text preview for `text/plain`, `text/csv`, PDF, and Office Open XML documents, via the message attachment preview endpoint. Visible text only. Paid entitlement (Starter+). |
| `raw_download_selected_types` | Metadata, previews, plus raw byte download for selected families (`pdf`, `text`, `csv`, optional `image`). Session-cookie auth + TOTP/password re-auth + Pro+ entitlement; raw `image` needs a separate image-risk acknowledgement. |

```json
{
  "mode": "derived_content",
  "allowed_file_families": ["pdf", "text", "csv"],
  "accept_disclaimer_version": "v2026-07"
}
```

`allowed_file_families` (closed enum `pdf | text | csv | image`) is required for
`raw_download_selected_types` and ignored for the other modes; a `'*'` wildcard is
rejected `400 INVALID_FILE_FAMILY_WILDCARD`. Adding `image` requires the current
image-risk acknowledgement.

On `derived_content`, extracted preview text is scanned before it can be served;
a preview whose extracted-text scan is rejected returns `403
ATTACHMENT_PREVIEW_BLOCKED`. On `raw_download_selected_types`, an agent downloads
raw bytes for an approved family from `GET /v1/messages/:id/attachments/:idx` —
those bytes are AV-clean and policy-clean but are not scanned for hidden
instructions, so the agent must treat them as untrusted.

**Re-auth applies only when enabling or widening approved downloads.** Narrowing to
a subset of already-approved families, or downgrading to `metadata_only` /
`derived_content`, does not require re-auth and is a valid admin-key path.

**Errors:** `400` family/disclaimer/re-auth validation (e.g.
`INVALID_FILE_FAMILY`, `EMPTY_ALLOWED_FILE_FAMILIES`, `DISCLAIMER_VERSION_MISMATCH`,
`REAUTH_INVALID_TOTP`), `403 INSUFFICIENT_SCOPE` / `REAUTH_REQUIRES_SESSION` /
`TIER_LIMIT` / `ATTACHMENT_ACCESS_INELIGIBLE`, `404 NOT_FOUND`, `429 RATE_LIMITED`
(re-auth brute-force lockout). The legacy `{ "enable": true }` shape is deprecated
(`400 LEGACY_ENABLE_DEPRECATED`); `{ "enable": false }` remains as a kill-switch to
`metadata_only`. Full remediation: [error catalog](/agents/errors).

## POST /v1/mailboxes/:id/outbound-attachments — enable outbound attachments

**Classification:** `mutating` · **Auth:** **session + fresh re-auth only** to
enable — a Bearer key gets `403 REAUTH_REQUIRES_SESSION`. Disabling is admin-only.
Pro+ entitlement.

Turns on the mailbox's ability to send attachments, with a versioned disclaimer
acceptance. A human account owner enables it once (with TOTP/password re-auth);
after that, API keys can stage and send attachment handles normally.

```json
{
  "enabled": true,
  "accept_disclaimer_version": "v2026-07",
  "accept_image_risk_version": "v2026-05",
  "reauth_token": "<session re-auth token>"
}
```

The response echoes `outbound_attachments_enabled` and the accepted-version
timestamps (image fields `null` when the image-risk version was not supplied).

**Errors:** `403 REAUTH_REQUIRES_SESSION` (Bearer used) /
`OUTBOUND_ATTACHMENTS_INELIGIBLE` (below Pro+), `400` re-auth/disclaimer validation,
`404 NOT_FOUND`. The staging-and-send lifecycle for the handles themselves — upload,
scan, consume-once — is the [attachments contract](/agents/attachments).

## PATCH /v1/mailboxes/:id/sender-policy — flip the inbound firewall mode

**Classification:** `mutating` · **Auth:** admin **or** mailbox-bound `agent` key
(the customer is being protected *from* senders, so both roles can tighten).

Flips the mailbox's inbound firewall between `blocklist` and `allowlist`.

```json
{ "mode": "allowlist", "force_empty": false }
```

`force_empty` is required when flipping to `allowlist` while the inbound allowlist
is empty; without it the flip returns `409 SENDER_POLICY_FLIP_EMPTY_ALLOWLIST`.
Passing `true` acknowledges that every incoming sender will be `firewall_blocked`
until entries are added.

**Response (200):**

```json
{
  "mailbox_id": "...",
  "sender_policy_mode": "allowlist",
  "previous_mode": "blocklist",
  "changed_at": "2026-04-25T20:35:06Z"
}
```

A `mailbox.sender_policy_changed` webhook fires only on real flips (no-op flips are
suppressed) — see the [webhook catalog](/agents/webhooks).

## Instruction-trust operations

Trusted instruction sources are an opt-in, default-off **read-path** relaxation:
once an operator configures them, a configured `agent` key reading a verified
inbound message from a trusted sender receives narrower guidance in
`agent_safety_context`. The per-field trust taxonomy — what the relaxed
`agent_safety_context` and its `instruction_trust` basis contain — is owned by the
[security model](/agents/security-model).

Auth here is **asymmetric by design**. Every operation that *loosens* trust
(granting a source, enabling the mailbox mode, turning the strict-recipient guard
**off**) requires a **dashboard session with fresh re-auth** and rejects Bearer keys
with `403 REAUTH_REQUIRES_SESSION`. **An agent cannot enable, grant, or loosen
instruction trust for itself.** Only *tightening* (revoke, disable, turn the guard
**on**) is unprivileged and agent-callable, so an agent can respond fast to an
incident.

### GET /v1/mailboxes/:id/trusted-sources — list trusted sources

**Classification:** `read-only` · **Auth:** session or any mailbox-scoped `agent`
key.

Returns `{ "mailbox_id": ..., "sources": [ ... ] }`. Each source carries `grain`
(always `"address"` in v1), `value`, `value_ascii` (punycode/ASCII rendering — a
transparency surface for homoglyph domains, not the match key), `expires_at`,
`revoked_at`, a derived `status` (`active | revoked | expired`), and an
`eligibility_snapshot`.

### POST /v1/mailboxes/:id/trusted-sources — grant a trusted source (loosening)

**Classification:** `mutating` · **Auth:** **session + fresh re-auth only** (Bearer
→ `403 REAUTH_REQUIRES_SESSION`).

Grants one verified sender address as a trusted instruction source. v1 is
**address-grain only** — a domain grain is rejected
`400 INSTRUCTION_TRUST_DOMAIN_GRAIN_UNSUPPORTED`. Provide exactly one of `value` (the
typed address) or `message_id` (a verified inbound message the server derives the
address from). Both modes require prior evidence of receipt: a verified inbound
message from that exact address must already exist, or the grant fails
`422 TRUSTED_SOURCE_NO_VERIFIED_HISTORY` / `TRUSTED_SOURCE_ANCHOR_NOT_ELIGIBLE`.

```json
{
  "value": "alice@partner.com",
  "reauth_token": "...",
  "residual_ack_version": "v2026-06-a",
  "address_attestation": true,
  "expires_in_days": 90
}
```

`expires_in_days` defaults to 90 and must be an integer 1–365 (no infinite grants).
The gate order is session re-auth → an accountability floor (an active billing
entitlement or an exemption; failing it returns
`403 INSTRUCTION_TRUST_ACCOUNTABILITY_REQUIRED`, a non-monetary check with no upgrade
link) → the current residual-risk acknowledgement
(`400 INSTRUCTION_TRUST_RESIDUAL_ACK_REQUIRED`) → `address_attestation: true`
(`400 INSTRUCTION_TRUST_ADDRESS_ATTESTATION_REQUIRED`) → the evidence-of-receipt
check.

**Response (201):** the created source object. Granting an address already actively
trusted returns `409 TRUSTED_SOURCE_ALREADY_EXISTS`; granting a previously revoked
or expired entry transparently revives the same row. Fires the
`trusted_source.granted` [webhook](/agents/webhooks).

### DELETE /v1/mailboxes/:id/trusted-sources/:sourceId — revoke (tightening)

**Classification:** `mutating` · **Auth:** unprivileged — any mailbox-authorized
caller, including an `agent` key.

Soft-revokes a trusted source (stamps `revoked_at`; the row and its ledger lineage
are retained). Idempotent — revoking an already-revoked source returns the same
shape with `already_revoked: true` and does not re-fire the webhook. Returns
`404 TRUSTED_SOURCE_NOT_FOUND` if the source does not exist. Fires the
`trusted_source.revoked` [webhook](/agents/webhooks).

### POST /v1/mailboxes/:id/instruction-trust — mailbox master switch

**Classification:** `mutating` · **Auth:** **enabling** is loosening — session +
fresh re-auth only, plus the accountability floor, the current residual-risk
acknowledgement, and an **explicit** `strict_recipient` choice. **Disabling** is
unprivileged and agent-callable.

**Enable:**

```json
{ "enabled": true, "reauth_token": "...", "residual_ack_version": "v2026-06-a", "strict_recipient": true }
```

**Disable:** `{ "enabled": false }` (does not touch the stored `strict_recipient`
value).

**Errors:** `400 VALIDATION_ERROR`, `404 NOT_FOUND`, the re-auth ladder,
`403 INSTRUCTION_TRUST_ACCOUNTABILITY_REQUIRED`,
`400 INSTRUCTION_TRUST_RESIDUAL_ACK_REQUIRED`,
`400 INSTRUCTION_TRUST_STRICT_CHOICE_REQUIRED`. See the
[error catalog](/agents/errors).

### POST /v1/mailboxes/:id/instruction-trust/strict-recipient — send-path narrowing

**Classification:** `mutating` · **Auth:** turning it **on** (`true`) is tightening
— unprivileged and agent-callable. Turning it **off** (`false`) re-widens the send
surface and requires session + fresh re-auth.

When the mailbox's trust mode is enabled, `strict_recipient: true` suppresses the
thread-scoped reply bypass for **agent-origin** sends — an agent send is then
admitted only via an explicit allowlist entry, never inferred from thread history.
It defaults `true` (contained at rest) and is inert unless the trust mode is
enabled. The interaction with the allowlist bypass is documented at
[send gates](/agents/send-gates).

```json
{ "strict_recipient": false, "reauth_token": "..." }
```

(`reauth_token` is only required to turn it off.)

---

Per-key instruction-trust capability is a **key** operation, not a mailbox one —
enabling it likewise requires a session with fresh re-auth and only applies to
`agent`-role keys; see the [security model](/agents/security-model) for how the
mailbox mode, the key capability, and a grant combine to relax a read.
