# Email for Agents documentation: agent email API reference
URL: https://emailforagents.ai/docs
Summary: Reference for the agent email API: base URLs, API key and MCP authentication, scopes, every error code with its HTTP status, and the limits enforced in code.

1. [Home](https://emailforagents.ai/)
2. Docs

Docs

## Email for Agents documentation

Give an AI agent its own email address, then let it read, draft and send through a scoped credential you can revoke. This page is the shared reference the rest of the documentation builds on; every number below is read out of the implementation, not the marketing copy.

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

If you have not created an inbox yet, start with the [two-minute quickstart](https://emailforagents.ai/docs/quickstart). If you are deciding whether to build on the REST API or connect over MCP, the
[agent email API overview](https://emailforagents.ai/product/developers) covers the product surface and
[MCP or API key for agent email](https://emailforagents.ai/blog/agent-email-mcp-or-api) compares the two
credential models side by side.

### Surfaces and base URLs

| Surface | Base URL | Who calls it
| REST API | `https://api.emailforagents.ai` | Your own code, with a project-scoped API key
| MCP server | `https://mcp.emailforagents.ai/mcp` | An MCP client holding an OAuth connection grant
| Console | `https://app.emailforagents.ai` | A signed-in human: inboxes, approvals, keys, grants, policy

The API version string is `2026-09-19`. It appears on every event record and on every
webhook body, so a consumer can branch on it rather than guessing from the payload shape.

### Authenticating a request

There are two machine credentials and they are not interchangeable. Which one you hold decides
which surface you may call.

#### Project-scoped API keys

Create a key in **Access**. The plaintext is shown once. Its shape is
`efa_test__` for a test project and
`efa_live_…` for a live one, so the environment is legible in a log line without
resolving the key. Send it as `Authorization: Bearer efa_live_…`. Only the digest is
stored, which is why a lost key must be replaced rather than recovered. Revoke one with
`POST /v1/keys/{key_id}/revoke`.

#### Connection grants for MCP

MCP clients authenticate with an OAuth connection grant, obtained through a browser consent
step in which a human picks the specific inboxes and the permission preset. An
`efa_` API key presented at `/mcp` is rejected, and so is a console
session token: only a principal of type `connection` may invoke MCP tools. That is
deliberate, not a gap — an MCP client is software a human authorised for named inboxes, and the
grant carries that authorisation. The server identifies itself as
`email-for-agents-mcp` 1.0.0 and negotiates protocol versions
`2025-11-25`, `2025-06-18` and `2025-03-26`. A
`GET` to the endpoint returns `405` with `Allow: POST`.

**No MCP client is certified or one-click here.** Connections are recorded with a
trust class of `unverified` unless an operator has classified them otherwise, and
no client has been through a verification process. Connect one, then prove a round trip in a
test project before you rely on it.

### Scopes and connection presets

Authority is re-derived on every request and intersected with the authorising human's current
grant, so demoting a member or revoking a key narrows what an existing credential can do without
any cache to wait for. There are 32 scopes; sending uses `messages:send`, submitting
a draft for review uses `drafts:submit`, and reading events uses
`events:read`.

MCP consent offers three presets, and each maps to a fixed scope set:

| Preset | OAuth scope | Scopes granted
| `read` | `email.read` | `inboxes:read`, `threads:read`, `messages:read`,
`attachments:read`, `events:read`
| `read_draft` | `email.draft` |
the `read` set plus `drafts:read`, `drafts:write`,
`drafts:submit`
| `read_draft_send` | `email.send` | the `read_draft` set plus `messages:send`

Administrative scopes can never be attached to a connection: `keys:manage`,
`members:manage`, `billing:manage`, `policies:write`,
`inboxes:delete`, `messages:delete`, `messages:raw` and
`exports:write` are refused on a grant. A connection holding only
`drafts:submit` is treated as draft-only: its sends become approval requests even
when the inbox permits direct sending. See
[sending controls and approvals](https://emailforagents.ai/docs/sending-controls) for how that interacts with
inbox policy.

### Resource ID prefixes

Every identifier is a prefix plus a UUID, and the prefix is validated on input. Passing a thread
ID where an inbox ID belongs fails immediately rather than resolving to something plausible.

| Prefix | Resource | Prefix | Resource
| `ws_` | Workspace | `prj_` | Project
| `ibx_` | Inbox | `thd_` | Thread
| `msg_` | Message | `drf_` | Draft
| `apr_` | Approval | `att_` | Attachment
| `upl_` | Upload | `dom_` | Domain
| `key_` | API key | `con_` | Connection
| `evt_` | Event | `wh_` | Webhook endpoint
| `exp_` | Export job | `del_` | Deletion job

Inbox IDs use `ibx_`. Most routes are nested under both the project and the inbox —
for example `POST /v1/projects/{project_id}/inboxes/{inbox_id}/messages` — because
a credential is scoped to projects and inboxes, and the path is what that scope is checked
against.

### Request and response rules

- **Sends require an `Idempotency-Key` header.** Without one the request is
rejected with `invalid_request` before anything is created. See
[idempotency keys for agent email](https://emailforagents.ai/blog/retrying-agent-email-safely) for the replay
semantics.

- **JSON bodies are capped at 1 MiB** and read through a bounded reader, so an
oversized body is refused with `payload_too_large` rather than buffered. Attachment
content uploads are capped at 10 MiB instead.

- **List endpoints return `data`, `next_cursor` and `has_more`.** The default page is 25 items and `limit` accepts 1
to 100. A thread's message list defaults to 50. Cursors are covered in
[search and pagination](https://emailforagents.ai/docs/search).

- **A successful send returns HTTP 202** with
`result: "queued"`, or `result: "approval_required"` when policy or a
draft-only credential routes it to review. Neither means the message left the building yet.

- **Every error shares one envelope** and carries a request ID you can quote in a
support thread.

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

`retryable` is computed, not decorative: it is true for
`rate_limited`, `provider_unavailable` and
`temporarily_unavailable`, and false everywhere else unless the specific call sets it.
Retrying a non-retryable error changes nothing except your rate-limit budget.

### Error codes and HTTP statuses

The whole set, with the status each one maps to. Branch on `error.code`, not on the
status: several codes share a status and mean different things.

| Code | HTTP | What it means
| `invalid_request` | 400 | The body or headers do not match the schema.
| `invalid_cursor` | 400 | The cursor is malformed or belongs to another filter.
| `cursor_expired` | 410 | The events you were paging through are no longer retained. Restart without a cursor.
| `unauthenticated` | 401 | Missing, malformed or revoked credential.
| `payment_required` | 402 | The action needs an entitlement this workspace does not have.
| `quota_exceeded` | 402 | A plan quota, such as the custom-domain count, is used up.
| `permission_denied` | 403 | The credential lacks the scope, project or inbox for this call.
| `policy_denied` | 403 | Inbox, project or workspace policy blocked the send. Read `details.reason_codes`.
| `daily_limit_exceeded` | 403 | The daily recipient cap for this inbox is exhausted.
| `test_mode_required` | 403 | A simulation endpoint was called against a live project.
| `test_limit_exceeded` | 403 | A test-mode cap would be exceeded: 3 active inboxes, 100 retained messages or 10 MiB.
| `not_found` | 404 | No such resource, or none you are authorised to see.
| `source_unavailable` | 404 | The underlying content could not be read.
| `idempotency_conflict` | 409 | The key was reused with a different canonical payload.
| `idempotency_expired` | 409 | The 30-day replay window for that key has closed.
| `address_unavailable` | 409 | The requested address is taken or reserved.
| `send_already_submitting` | 409 | A send with this key is in flight.
| `last_owner_required` | 409 | The change would leave the workspace with no owner.
| `version_conflict` | 412 | Someone else changed the policy, endpoint or draft. Re-read and resubmit.
| `payload_too_large` | 413 | Body, upload or attachment set exceeds its limit.
| `recipient_suppressed` | 422 | A recipient is on the suppression list after a hard bounce or complaint.
| `domain_not_ready` | 422 | Ownership, sending or receiving verification has not completed.
| `attachment_not_ready` | 422 | A file has not finished a successful scan.
| `test_mode_unsupported` | 422 | The action does not exist in test mode, such as attaching a domain.
| `reserved_address` | 422 | The local part is reserved by the platform.
| `environment_immutable` | 422 | A project cannot move between test and live.
| `rate_limited` | 429 | Too many requests. A `retry-after: 60` header is sent.
| `provider_unavailable` | 503 | The upstream mail provider could not be reached.
| `temporarily_unavailable` | 503 | The service, or a dependency it needs to stay safe, is unavailable.

### Limits enforced in code

These are checked on the request path, not published as guidance. Exceeding one returns an error
rather than truncating your input.

| Limit | Value
| JSON request body | 1 MiB
| `text` and `html` body | 512,000 characters each
| Subject | 998 characters, and no CR or LF
| Unique recipients per message (To + Cc + Bcc) | 25
| Attachments per message | 10
| Bytes per attachment | 10 MiB
| Aggregate attachment bytes per message | 15 MiB
| Inbound raw MIME accepted | 25 MiB
| List page | 25 by default, 100 maximum
| Recipient allow or block entries per list | 100
| Idempotency replay window | 30 days
| Idempotency tombstone | 365 days
| Approval expiry | 7 days
| Reserved upload expiry | 24 hours
| Minimum interval between webhook test events | 60 seconds
| Active test inboxes per workspace | 3
| Retained test messages per workspace | 100
| Retained test bytes per workspace | 10 MiB
| Free live storage per workspace | 100 MiB

The free allowance is one complimentary live inbox and 100 email units a month per verified
owner identity, claimed by that identity's first eligible workspace. Creating a second workspace
does not renew it. [Pricing](https://emailforagents.ai/pricing) has the full wording.

### Rate limits

Three counters run in production: 1,200 requests per 60 seconds per client IP at the edge, 600
per 60 seconds per principal, and 600 per 60 seconds per workspace. Exceeding any of them
returns `rate_limited` with `retry-after: 60`.

Treat those numbers as a ceiling rather than a contract. The counters are Cloudflare rate-limit
bindings, which are local to the edge location serving the request and deliberately permissive;
the code that reads them says as much. They are protection against a runaway loop, not a metering
system, and they are never billing authority.

### Test mode and live mode

A project is created as `test` or `live` and cannot move between them; the
attempt returns `environment_immutable`. The environment decides the transport.

| | Test project | Live project
| Address | Generated, on a non-routable `.mail.invalid` host | A real address on `agents.emailforagents.ai` or your own verified domain
| Transport | A local fake provider. Nothing reaches the internet. | The upstream mail provider
| Inbound | `POST /v1/projects/{project_id}/test/inbound` synthesises a received message | Real mail delivered to the address
| Caps | 3 active inboxes, 100 retained messages, 10 MiB | Entitlement, storage and daily recipient caps
| Custom domains | Not supported | Requires a paid subscription

`Test inboxes accept no custom local part or domain: the address is generated, and asking for one returns test_mode_unsupported`. That is what makes test mode safe to point an
unproven agent at. [Testing an agent's email without sending real mail](https://emailforagents.ai/blog/test-agent-email-without-sending) walks through a full rehearsal.

### What is not available yet

- **Paid checkout is disabled.** Builder and Team are proposed prices and cannot be
bought today. Anything gated behind a paid subscription — custom domains, higher quotas — is
therefore gated in practice too.

- **No published SDK.** There is no package to install; the examples here are curl
and plain HTTP on purpose.

- **No certified MCP client.** Generic remote MCP with OAuth is the supported shape;
no named client has been verified.

- **Customer-triggered event replay is not exposed.** Recover missed events with the
durable events cursor instead.

- **Several event types are declared but not emitted by this build.** The
[event catalogue](https://emailforagents.ai/docs/events-and-webhooks#catalogue) marks which is which, so you
do not build a branch that never runs.

- **There is no published uptime SLA and no status feed.** Provider acceptance is
also not proof of delivery; see [status](https://emailforagents.ai/status) and
[security](https://emailforagents.ai/security).

### Where to go next

[Quickstart — Create an inbox, connect an agent, send your first message.](https://emailforagents.ai/docs/quickstart)

[Sending controls & approvals — Recipient allow-lists, pause, daily caps, human review.](https://emailforagents.ai/docs/sending-controls)

[Events & webhooks — The event catalogue, signature verification, retry schedule.](https://emailforagents.ai/docs/events-and-webhooks)

[Attachments — Reserve, upload, scan, send and download files safely.](https://emailforagents.ai/docs/attachments)

[Search & pagination — What the index covers and how signed cursors behave.](https://emailforagents.ai/docs/search)

[Custom domains & DNS — The exact TXT, MX, SPF and DKIM records, and why order matters.](https://emailforagents.ai/docs/domains)

[Exports & data controls — NDJSON exports, their exclusions, deactivation versus erasure.](https://emailforagents.ai/docs/data-controls)

[Email for AI agents — The conceptual guide behind all of the above.](https://emailforagents.ai/blog/email-for-ai-agents)

[Next Quickstart](https://emailforagents.ai/docs/quickstart)
