# Plans

These are the limits the API enforces.

## Plans

Every plan has unlimited inboxes. Plans differ in how much mail and storage they include; see [pricing](/content/pricing/index.html) for prices and overage rates.

| Plan | Messages included a month | Daily limit | Storage | Custom domains | Seats |
| --- | --- | --- | --- | --- | --- |
| Free | 3,000, inbound and outbound combined | 100 messages | 1 GB | 1 | 1 |
| Pro | 7,500 inbound, 2,500 outbound | None | 10 GB | 10 | Unlimited |
| Team | 45,000 inbound, 15,000 outbound | None | 30 GB | 50 | Unlimited |
| Scale | 200,000 inbound, 65,000 outbound | None | 150 GB | 250 | Unlimited |

An outbound message counts once per recipient. Spam and quarantined mail never count. On Free, inbound mail past the allowance is held until it resets or you upgrade, and sending pauses until then. Upgrade in the dashboard under Billing.

## How plan limits are enforced

### What counts

- **Periods.** Allowances are per calendar month in UTC and reset at 00:00 UTC on the first day of the next month. The Free daily limit resets at 00:00 UTC every day.
- **Inbound.** Each inbound message counts once, including mail injected with `POST /inbound/raw` (even when it is marked spam or quarantined: its content is yours) and mail your own receive lists block. Spam and quarantined mail never count; a message you move out of spam or quarantine (by removing the `SPAM` or `QUARANTINE` label) counts from then on. Mail an agent’s sandbox inbox received before you [claimed](/content/docs/reference/limits#agent-sandbox/index.html) it counts toward the month it is claimed in.
- **Outbound.** Each sent message counts once per distinct recipient across `to`, `cc` and `bcc`: a send to 5 addresses counts 5. Sends, replies, reply-all, forwards and drafts (scheduled or not) all count. A send refused before it goes out (suppressed or blocked recipient, a limit) does not count; a send that bounces after it was accepted does.
- **Storage.** The size of your stored messages, your drafts (body and attachments) and held mail. Spam and quarantined mail do not count, up to 1,000 MB of it per organization. Past that, on Free new spam and quarantined mail is not stored (`message.rejected` with reason `plan_limit`); on paid plans it counts toward your storage usage but is never billed. 1 GB is 1,000,000,000 bytes.
- **Deleting mail** frees storage but does not give back messages already counted this month.

Check your usage with [`GET /v0/organizations/usage`](/content/docs/reference/api/organization/index.html), `machinemail org usage`, or the Usage page of the dashboard.

### Free plan

| Limit reached | What happens |
| --- | --- |
| 3,000 messages a month or 100 messages a day | Sends are refused with `402 plan_limit_exceeded` ( [errors](/content/docs/reference/errors#plan-limits/index.html)), with `resets_at` and a `Retry-After` header. Nothing is sent. A send needing more recipients than are left is refused whole. Scheduled drafts stay scheduled and go out after the reset. Inbound mail is [held](/content/docs/reference/limits#held-mail/index.html). |
| 1 GB of storage | Inbound mail is [held](/content/docs/reference/limits#held-mail/index.html) until you delete mail or drafts, or upgrade. Creating or growing a draft returns `402 plan_limit_exceeded` (`limit_key: "storage_bytes"`). Sending still works. |
| 1 custom domain | Adding another domain returns `402 plan_limit_exceeded` (`limit_key: "custom_domains"`). |
| 1 seat | Inviting returns `402 plan_limit_exceeded` (`limit_key: "seats"`). Members and pending invitations both use a seat. |
| 10 trusted domains | Adding another [trusted domain](/content/docs/concepts/trusted-domains/index.html) returns `402 plan_limit_exceeded` (`limit_key: "trusted_domains"`). Paid plans allow 500, and are refused the same way past them. |

When both the monthly and the daily limit apply, the error reports the monthly one. A send to more recipients than the limit itself can never fit: its error has no `resets_at`.

### Held mail

Held mail is stored but not delivered to your agents:

- It is not listed, searchable or exported, and getting it by ID returns `404`.
- No `message.received` event, webhook, realtime event or MCP notification fires, and `wait_for_message` does not see it.
- Once any mail is held for an inbox, new mail to that inbox waits behind it, so an inbox’s mail is always delivered in the order it arrived. This holds on every plan, also right after an upgrade.
- The dashboard shows a banner with the held count, and `GET /v0/organizations/usage` reports it in `held`. `POST /inbound/raw` lists held copies in `held` instead of `messages`.

Held mail is delivered, oldest first, within about a minute of any of these:

- the daily limit resets (mail held for the daily limit) or the monthly one does;
- you free up storage (mail held for storage);
- your organization moves to a paid plan.

On delivery, `message.received` fires as usual. The message’s `created_at` is the delivery time and `held_at` is when it arrived. It joins its thread as of delivery, so replies that arrived meanwhile thread correctly.

Delivered mail counts toward the month and the day it is delivered in, so a backlog is delivered at most 100 messages a day (the daily limit), oldest first, while new mail queues behind it. Mail held for storage is delivered one message at a time as it fits next to the mail you keep.

| Held mail limit | Value |
| --- | --- |
| Held at once | 3,000 messages or 1,000 MB per organization, whichever comes first. Inbound mail past it is not stored: `message.rejected` fires with `reason: "plan_limit"`. |
| How long it is kept | 35 days from the reset that could deliver it (mail held for storage: from its arrival). After that it is deleted and `message.rejected` fires with `reason: "plan_limit_expired"`. |

### Paid plans

Pro, Team and Scale are never blocked. Usage past an allowance is counted and billed as overage at the rates on the [pricing page](/content/pricing/index.html): inbound per 1,000 messages, outbound per 1,000 recipients, storage per GB-month, and custom domains past the plan’s number per domain-month. Spam and quarantined mail are never billed.

- **When.** Overage is billed monthly in arrears, at the end of each billing period, for the usage in that period. A paid plan’s billing period runs monthly from the day you subscribed, so it can differ from the calendar month that `GET /v0/organizations/usage` reports.
- **Storage** is billed on your average storage over the period (GB-month), from a daily reading.
- **Custom domains** are billed on the most domains you had at once during the period, from a daily reading.
- **Plan changes.** An upgrade takes effect right away, with the new allowances, and the price difference for the rest of the period is charged pro rata. A downgrade takes effect at the end of the period. Moving to Free keeps your data: existing domains and members stay, and Free limits apply to new usage.

### Already over a limit

Each account can own one Free organization. Creating another returns `402 plan_limit_exceeded` (`limit_key: "free_organizations"`); organizations on a paid plan do not count, and joining other people’s organizations is not limited.

Limits apply to new additions only. An organization with more custom domains or members than its plan allows (after a move to Free, for example) keeps them; it cannot add more until it is under the limit. An invitation is checked when it is sent, so an invitation sent earlier can still be accepted.

The [agent sandbox](/content/docs/reference/limits#agent-sandbox/index.html) has its own limits: plan limits do not apply to unclaimed sandbox inboxes. Once claimed, the inbox belongs to an organization and that organization’s plan applies; the mail it received so far counts toward that month, and on Free a claim that does not fit in the month’s allowance returns `402 plan_limit_exceeded` and nothing moves.

## Inboxes and addresses

| Limit | Value |
| --- | --- |
| Number of inboxes | No limit on your own domains. Inbox count is not a quota. |
| Inboxes on the shared domain (`machinemail.dev`) | 1,000 per organization. Past that, creating one returns `403 Inbox limit reached for <domain> (1000 per organization). Add your own domain for more.` |
| Username | 1 to 64 characters: `a-z`, `0-9`, `.`, `_`, `+`, `-`; must start and end with a letter or digit. Input is trimmed and lowercased. |
| Username when omitted | Random, 12 characters. |
| Address uniqueness | Global. An address taken by any organization returns `409 Inbox with email <email> already exists`. |
| Domain on create | Defaults to the shared domain. Any other domain must belong to your organization (`404 Domain not found` otherwise). |
| Metadata | String values. No size or key-count limit is enforced. |

Addresses on the shared domain are first come, first served across all organizations. Mail to an unknown address on the shared domain is never turned into a new inbox.

## Ephemeral inboxes

| Setting | Value |
| --- | --- |
| `ttl_seconds` | Any positive number of seconds. There is no maximum. |
| `ephemeral: true` without `ttl_seconds` | TTL of 3600 seconds (one hour). |
| Expiry | `expires_at = created_at + ttl_seconds`. Every inbound message moves it to `now + ttl_seconds` (never earlier). |
| Changing it | `PATCH` with a new `ttl_seconds` restarts the clock from now. `ttl_seconds: null` or `ephemeral: false` makes the inbox permanent. |
| Deletion | An expired inbox is deleted with its messages, threads, drafts and files within about a minute (the reaper runs every 60 seconds), and `inbox.expired` is emitted. |

## Catch-all domains

A custom domain with `catch_all: true` creates an inbox the first time mail arrives for an unknown address, when all of these hold:

- the domain is `VERIFIED` and `inbound_enabled` is true;
- the local part is a valid username.

The new inbox has `created_by: "catch_all"`. If the domain sets `catch_all_ttl_seconds` (any positive number), the inbox is ephemeral with that TTL; otherwise it is permanent. `inbox.created` is emitted.

## Messages and attachments

| Limit | Value |
| --- | --- |
| Recipients per message | At least one of `to`, `cc`, `bcc`, and at most 50 across all three, each address counted once (case-insensitive). An unclaimed [agent sandbox](/content/docs/reference/limits#agent-sandbox/index.html) inbox can have at most 5. More returns `400 Too many recipients: <n> across to, cc and bcc. A message can have at most 50 recipients; send separate messages instead.` (`param: to`). Applies to sends, replies, reply-all, forwards, drafts (checked on create, update and send) and scheduled drafts (over the cap at send time: `send_status: failed`). An entry holding an address list (`a@x.com, B <b@y.com>`) counts, and is checked against suppressions and send lists, as each address in it. Each address must look like `name@host`. |
| Attachment by `url` | Fetched by MachineMail: public `http(s)` only, no redirects, at most 10 MB, 20 second timeout. |
| Attachment by base64 `content` | Valid base64; counts toward the per-message attachment limit. |
| Attachments per message | At most 100, and 10 MB of files in total (decoded, `content` and `url` together). More is a `400 validation` error with `param: "attachments"`; nothing is sent. |
| Request body (`/v0`) | 16 MB. Larger bodies are refused with `400` before they are read. |
| Raw message injection (`POST /inbound/raw`) | 40 MB. |
| Preview (`preview` field) | First 200 characters. |
| Attachment `download_url` | Expires one hour after the request. |
| Suppressed recipients | Sending to an address in [suppressions](/content/docs/reference/api/suppressions/index.html) returns `403`. Permanent bounces and complaints add addresses automatically. |
| Paused inbox | Cannot send (`403`) or receive (mail is not stored; `message.rejected` is emitted). |

## Batches, lists and search

| Limit | Value |
| --- | --- |
| Page size (`limit`) | Default 50, maximum 500. See [Pagination](/content/docs/reference/pagination/index.html). |
| `batch-get`, `batch-update` | 100 message IDs per request; extra IDs are dropped. |
| Thread detail | Up to 500 messages per thread in `GET /v0/threads/{thread_id}`. Thread label updates touch up to 500 messages. |
| Thread search | Scans the newest 500 matching messages (`truncated: true` when more). |
| Inbox search (`GET /v0/inboxes/search`) | Scans the newest 500 inboxes. |
| Allow and block lists | Up to 500 entries per direction are applied to mail (the first 500 in entry order). |
| Metrics | `period` 1 to 86400 seconds; default range the last 30 days; `start` must be before `end`; at most 100,000 events counted per query. |
| Export (`GET /v0/inboxes/{inbox_id}/export`) | No limit: streams every message as `mbox` or `jsonl`. |

## Webhooks and events

| Limit | Value |
| --- | --- |
| Webhooks per organization | No limit. Events are matched against the first 500 webhooks of an organization. |
| Webhook URL | Public `http(s)`; private and loopback addresses are refused. |
| Delivery timeout | 10 seconds. |
| Retries | 1 m, 5 m, 30 m, 2 h, 12 h, then daily, for up to 5 days. See [Webhook events](/content/docs/reference/webhook-events#retries/index.html). |
| Stored response body | First 1024 bytes. |
| SSE resume (`Last-Event-ID`) | Up to 500 missed events. |
| SSE keepalive | `: ping` after 15 seconds without events. |
| WebSocket idle timeout | 120 seconds without traffic. Send `{"type":"ping"}` to keep it open. |
| MCP `wait_for_message` | Up to 60 seconds per call (default 30). |
| MCP `send_feedback` | `message` of 1 to 2,000 characters. See [Rate limits](/content/docs/reference/rate-limits#mcp-feedback/index.html). |

## Domains

| Limit | Value |
| --- | --- |
| Domains per organization | Set by your plan: Free 1, Pro 10, Team 50, Scale 250. Paid plans can go over at $1 per domain per month ( [pricing](/content/pricing/index.html)). |
| Domain name | A valid hostname, 3 to 253 characters. |
| Uniqueness | A domain can belong to one organization only (`409 Domain <domain> is already registered`). The shared domain cannot be added (`409 <domain> is a shared domain`). |
| DNS checks | `PENDING` and `INVALID` domains are re-checked in the background at most every 5 minutes, for 7 days after the domain was added. After that, call `POST /v0/domains/{domain_id}/verify`. Domain Connect discovery is cached for 6 hours (10 minutes after a provider error). |

## Pods

- The default pod cannot be deleted.
- A pod with inboxes cannot be deleted (`409 Pod still has inboxes; delete them first`).

## Agent sandbox

An agent that registers itself anonymously through [auth.md](/content/docs/reference/authentication#agent-access-tokens/index.html) gets, until a person claims it:

| Limit | Value |
| --- | --- |
| Inboxes | One sandbox inbox on the shared domain, `agent-<12 characters>@machinemail.dev` |
| Sandbox inbox lifetime | 24 hours |
| Messages | 10 per rolling 24 hours |
| Different recipients | 10 per rolling 24 hours (writing again to an address already counted is free) |
| Recipients per message | 5 (`to`, `cc` and `bcc` together; never more than the cap for every message) |
| Scopes | `inboxes.read`, `messages.read`, `messages.send`, `messages.write` on that inbox only |
| Claim window | 24 hours |
| Access token lifetime | 1 hour |
| Registrations | 1 per rolling hour and 3 per rolling 24 hours per IPv4 address or IPv6 /64, and 10 per rolling 24 hours per IPv6 /48; about 100 per rolling hour and 1000 per rolling 24 hours across the service |
| Unclaimed sandboxes | About 500 live across the service; new anonymous registrations wait while it is full |

Every send counts when it is made: deleting sent messages or threads does not give any back. Sends to disposable email addresses (throwaway inboxes) can be refused with `403` and `error.code` `disposable_email_not_allowed` (`param` is the field holding the address: `to`, `cc` or `bcc`). Claiming the agent lifts the sandbox caps and this rule. See [Rate limits](/content/docs/reference/rate-limits#sandbox-sends/index.html).

## Credentials

| Item | Lifetime |
| --- | --- |
| API key | Until revoked, or until its `expires_at` |
| Dashboard session | 30 days |
| Sign-in link | 15 minutes, single use |
| First API key display (`GET /auth/first-key`) | 10 minutes after first sign-in, once |

## Retention

Messages, raw MIME, attachments, events and webhook deliveries are kept until you delete them (or the inbox expires). There is no automatic retention window today.
