Inboxes · September 23, 2026

What Happens When an Agent's Email Allowance Runs Out

Sending returns HTTP 402, not 429. What keeps working, how a unit is counted, which reservations stay held, and the upstream quota you do not control.

An agent that runs a mailbox will eventually reach the end of its monthly allowance, usually mid-task, usually while something else is waiting on the reply. Most providers publish the quota number and stop there. The number is the least useful part. What matters is which capabilities survive the moment the counter hits zero, which units are still held rather than spent, and whether your retry loop makes the situation worse.

This page sets out exactly what happens when an agent’s email allowance runs out here: which call fails, with which status code, what keeps working, how the counter is spent, and the second allowance sitting above yours that no dashboard shows you. If agent mailboxes are new to you, start with email for AI agents. If you only want plan numbers, the pricing page has them.

What happens when an agent’s email allowance runs out

Sending stops. Nothing else does.

A live send whose recipients would take the workspace past its allowance is refused before anything is submitted to a provider. The API returns HTTP 402 with the error code quota_exceeded and the message “This workspace has exhausted its delivery allowance.” No row reaches a provider, there is no partial send, and no overage is billed, because there is no overage path to bill through. The refusal is a single conditional UPDATE in reserve() in packages/domain/src/billing.ts, guarded by used + reserved + quantity <= allowance + additional_allowance. If that statement updates no row, the request throws. quota_exceeded maps to 402 in packages/contracts/src/errors.ts.

Everything the agent can do without transmitting keeps working:

CapabilityAfter the allowance is exhausted
Reading threads and messagesworks
Receiving new inbound mailworks, and is still recorded
Creating draftsworks
Submitting a draft for human approvalworks
Polling the project event streamworks
Webhook deliverieswork
Exporting mail as JSONworks
Sending, or approving a sendHTTP 402 quota_exceeded

The agent degrades to read-only rather than going dark. A triage agent that classifies, labels and drafts continues to do all three, and only the final transmission is refused. That shape is a consequence of where the check lives: the quota is enforced at the send path, not at the session, the credential or the inbox. Nothing about an exhausted workspace changes what a token is allowed to do.

An email unit is one message in, or one recipient out

The unit is not “an email”. One message received is one unit, and one recipient sent to is one unit. A single send addressed to three people costs three.

The recipient count is taken across To, Cc and Bcc together, each address normalised, then deduplicated. Addressing the same person in To and again in Cc costs one unit, not two. The per-message ceiling is 25 unique recipients (LIMITS.uniqueRecipients in packages/contracts/src/events.ts), so the most a single send can cost is 25.

There is a trap in “normalised” that will surprise you, and it is worth knowing before it shows up in a bill. normalizeAddress in packages/contracts/src/canonical.ts trims the address and lower-cases the domain only. The local part is left exactly as you sent it, because the local part is case-sensitive under RFC 5321 and collapsing it would merge two addresses that a receiving server may treat as different people. The billing consequence follows directly:

Recipients on one sendUnique addressesUnits
ops@acme.com, ops@ACME.com11
ops@acme.com, Ops@acme.com22
ops@acme.com in To, ops@acme.com in Cc11

So an agent that copies a name out of a signature block and sends to Jane@corp.com while also sending to jane@corp.com pays twice, and the recipient sees two copies. This is not a rounding artefact that gets corrected later. On acceptance the ledger writes one debit row per distinct canonical address, and the settlement asserts that the count matches the number reserved, so the two-unit reservation becomes a two-unit charge. Lower-case the local part yourself before you build the recipient list if you want the collapse.

Test mail is never charged. The reservation call is wrapped in an inbox.environment === "live" check, so a send from a test inbox never touches the quota path at all, and injected inbound costs nothing either. That makes the whole of testing an agent’s email loop without sending free of quota consequences, which is most of the point of having a test environment.

Three consequences worth planning around:

Units are reserved before the send and settled afterwards

The counter moves in two steps, and the gap between them is where the confusing behaviour lives.

When a live send is queued, the unique recipient count is reserved against the current period. The reservation is what fails when there is no room, which is why the 402 arrives on the send call rather than minutes later. Once the provider outcome is known, the reservation settles:

OutcomeReservation stateUnits consumedWhy
acceptedconsumedone per recipientThe provider took the message.
failedreleasednoneNothing was transmitted.
submission unknownheld_unknownnone yetAcceptance could not be confirmed.
Recipient suppressedreversedone refundedA later provider event credits it back.

Two of those rows are load-bearing.

A failed send costs nothing. The reservation is released and the period is unchanged. You are billed for provider acceptance, not for attempts.

An unknown submission holds its units instead of releasing them. When a submission is interrupted after the request goes out but before an acknowledgement comes back, the intent lands in submission_unknown and the reservation moves to held_unknown rather than being decremented. That is deliberately conservative: the mail may well have gone out, and releasing the reservation would let the same allowance be spent twice. It has a consequence people miss. remaining is computed as allowance minus used minus reserved, so held units keep suppressing your headroom even though nothing has been charged. An inbox that accumulates a run of unknown submissions can report zero remaining with used still well under the allowance. Settlement is not permanently closed for those reservations, since the settle path only short-circuits on consumed and released, so a later confirmed outcome still resolves them. What acceptance actually proves covers how to diagnose that state.

A retried send carrying the same Idempotency-Key and the same payload replays the stored response and does not reserve a second time, so a retry loop cannot multiply your bill. It can still multiply your 402s. The rules for deriving keys are in retrying an agent’s email safely.

402, not 429: the distinction your retry loop depends on

Most email APIs answer an exhausted quota with HTTP 429, and that is the single most expensive assumption an agent can carry into this failure. Verified on vendor docs on 22 September 2026:

All defensible choices, and all of them collide with the default behaviour of nearly every HTTP client an agent framework ships with, because 429 means “try again later” and the standard handler obeys it. An agent that treats a monthly quota refusal as a rate limit backs off, retries, backs off further and retries again, for the rest of the calendar month, burning its own task budget against a condition that no amount of waiting inside the current run will clear.

This platform returns 402 for allowance exhaustion and reserves 429 for genuine rate limiting. Four different conditions stop a send, they have four different remedies, and only one of them is the monthly allowance. The classification you want in the agent:

ResponseMeaningRetryable
429 rate_limitedToo many requests right now.Yes. Back off.
402 quota_exceededThe delivery allowance is spent.No. Wait for the period reset.
402 payment_required “A current paid subscription is required to resume sending.”The workspace is not entitled to send at all.No. Different remedy entirely.
403 permission_denied “Sending is paused or the resource is inactive.”Workspace paused, or the inbox or project inactive.No. Checked before the quota.
403 policy_denied “The daily recipient limit has been reached.”A daily cap you configured, not the plan allowance.Yes, after the UTC day rolls.

The last two matter for diagnosis. The send authorization check refuses a workspace whose lifecycle, sending or abuse status is not clear before billing is consulted at all, so a paused workspace never reports as out of allowance. And the daily recipient limit is your own policy, bucketed by UTC calendar day, so it clears at 00:00 UTC regardless of where your billing period sits. Every ApiError carries a retryable boolean in the response body, and it is true only for rate_limited, provider_unavailable and temporarily_unavailable. Read that field rather than pattern-matching status codes.

There are two allowances, and the second one is not yours

Your workspace allowance is the one you can see. Above it sits the upstream sending provider’s own quota, on an account you do not hold, and the two fail in completely different ways. This is the failure mode nobody writes about, and it looks nothing like a 402.

When the upstream provider refuses a submission with 429 and a quota name in the body, the send is classified as retryable-before-submission rather than failed, because nothing was accepted. What follows is worth reading slowly:

  1. The intent goes back to queued, not to failed.
  2. Your reservation stays reserved. It is not released and not consumed, so those units remain unavailable to any other send.
  3. No event is emitted. message.queued already fired when you submitted, and the retry path deliberately skips event emission, so nothing on the webhook or the event stream tells you this happened.
  4. The retry waits for the provider’s Retry-After (or ratelimit-reset) when it sends one, whether that is shorter or longer than the defaults. Without either header, the wait is fifteen minutes for a quota refusal and one second for ordinary throttling. The job’s own exponential backoff, capped at five minutes, is the floor under both, and up to a second of jitter is added.
  5. The intent carries a provider idempotency deadline set to 23 hours after its first submission. When a still-queued intent passes that deadline, it is marked failed with last_error_code of provider_retry_window_expired, the reservation is released, and message.failed is emitted with that reason.
  6. Every retry is re-authorized from scratch. Before each attempt the send job re-runs the workspace, grant and policy checks and re-reserves the daily recipient limit against the current UTC day. A send that waited past 00:00 UTC moves its daily reservation into the new day’s bucket, and if that bucket is already full, or sending was paused or a policy changed in the meantime, the intent becomes canceled. The allowance reservation is released, and no event fires, because message.canceled is declared but not emitted in this build.

So a provider-side quota problem presents to your agent as a message that sits in queued for up to 23 hours with no event and no error, while holding units that your own counter reports as unavailable. The symptom you will actually observe is remaining dropping with used flat, and a message whose state never advances. That is the signature. Check remaining against used before you assume your own allowance is the problem, because if used + reserved is below the allowance, the block is not yours. And poll the message itself, not only the event stream: a send that ends this way in canceled looks, from the events alone, exactly like one still waiting.

Inbound mail is never dropped to protect the meter

Received mail is committed even when the workspace is past its allowance. Inbound is recorded against the usage ledger and increments the counter, and there is no branch anywhere in recordInbound() that refuses the message. The comment above that code says it plainly: provider-accepted mail is committed even beyond allowance, and is never dropped for billing.

Two things follow. First, used can exceed allowance, and the entitlement response exposes an over_limit boolean, true whenever used + reserved is greater than the allowance, for exactly that case. Second, the overshoot is real accounting rather than a free pass: mail received past zero has still spent units in the current period. No debt carries into the next one, but the deficit is not imaginary, it is simply not enforced against inbound.

This is a real divergence from at least one competitor. Nylas documents that on its Free and Essentials plans, exhausting the bandwidth budget pauses inbound delivery until the month resets, with overage billing instead of blocking on higher tiers. That is a coherent decision for a metered platform. It is not the one taken here, because an agent mailbox that stops accepting mail is a mailbox that loses somebody’s reply, and a lost reply is not recoverable by upgrading later.

An approval refused on quota is not spent

Approving a held draft runs the same send path as a direct send, with the authorisation kind set to human approval, which means it runs the same reservation and can throw the same 402. A reviewer who clicks Approve with an empty allowance gets quota_exceeded, and because the reservation and the approval state change happen in one transaction, the whole thing rolls back. The approval is not consumed, the draft is not marked sent, and the same approval can be used again once there is room.

There is a deadline on that, and it is shorter than a billing period. Approvals are created with expires_at seven days out, and the send path rejects an expired approval with permission_denied and the message “Approval is expired, consumed, or no longer valid.” If your allowance resets more than seven days after the approval was raised, the approval dies first and has to be requested again. An agent that parks approved work until the counter rolls over needs to re-submit the draft rather than re-use the approval. The mechanics of the approval surface itself are in holding agent email for human approval.

Check the counter before you send, not after

The entitlement endpoint returns the live period, so an agent can look before it leaps. The credential needs usage:read or billing:manage, and a credential with neither gets permission_denied rather than a partial response:

curl "https://api.emailforagents.ai/v1/billing/entitlement" \
  -H "Authorization: Bearer $EFA_API_KEY"

The usage block is the part you want:

{
  "usage": {
    "period_id": "go-live:2026-09",
    "period_start": "2026-09-01T00:00:00.000Z",
    "period_end": "2026-10-01T00:00:00.000Z",
    "used": 94,
    "reserved": 3,
    "allowance": 100,
    "remaining": 3,
    "over_limit": false
  }
}

remaining is allowance minus used minus reserved, floored at zero, so it already accounts for sends in flight and for anything held unknown. allowance already includes any additional grant on the period, so you do not have to add anything to it. One edge case to handle: usage is null when no period row exists yet, which is the state of a workspace that has never sent or received live mail. Treat null as unknown, not as zero:

import os, requests

BASE = "https://api.emailforagents.ai/v1"
H = {"Authorization": f"Bearer {os.environ['EFA_API_KEY']}"}

def remaining() -> int | None:
    usage = requests.get(f"{BASE}/billing/entitlement", headers=H).json()["usage"]
    return usage["remaining"] if usage else None

def affordable(recipients: list[str]) -> bool:
    left = remaining()
    if left is None:
        return True  # No period yet. Let the reservation decide.
    return len({r.strip() for r in recipients}) <= left

Do not treat that check as a guarantee. It is advisory, the authoritative decision is the reservation, and another agent in the same workspace can spend the difference between your check and your send. Use it to choose a behaviour, not to skip error handling. When remaining is small, a well-built agent switches to composing drafts and letting a human release them, which is the same degradation the 402 forces anyway, arrived at on purpose and with the human already in the loop.

What the free allowance covers, and when it resets

One complimentary live inbox and 100 email units a month. Both attach to a verified owner identity, not to a workspace, and they are claimed by that identity’s first eligible workspace. That binding is the answer to the obvious workaround: creating a second workspace does not renew the allowance, because the claim is recorded against the owner identity and the second workspace finds it already taken. The claim is a uniqueness constraint on the identity, which also means two concurrent activation attempts resolve to exactly one winner.

The free period is the calendar month in UTC. The period identifier is go-live:YYYY-MM, it runs from 00:00 UTC on the first to 00:00 UTC on the first of the next month, and the row is created on first use rather than on a schedule. There is no rollover, so unspent units do not carry forward. A workspace on a paid subscription follows that subscription’s own period instead, identified by the subscription rather than by the month, so its reset is not tied to the calendar. Nothing about a reset is retroactive: a send refused with quota_exceeded is not queued for the new period and is not retried for you. Submit it again.

Alongside the live inbox, test mode allows up to 3 active test inboxes per workspace, retaining the last 100 messages, up to 10 MB, with content expiring after 7 days. None of it is charged, and none of it can reach an internet recipient, because a test inbox is a non-routable .mail.invalid address on a simulated transport.

Builder at $19 a month and Team at $79 a month are proposed plans at proposed launch prices. Stripe checkout is disabled, so neither can be bought today, and final terms will be published before anything is charged. Custom domains require one of those plans, so every live address today is on the platform domain. If your workload needs more than 100 units a month right now, the honest answer is that this platform cannot sell you that yet. AgentMail’s free tier of 3 inboxes and 3,000 emails a month is more generous than ours on both axes, and for a high-volume workload today it is the better choice.

Three habits make 100 units last longer than you would expect. Develop against test inboxes, since the entire read, draft, approve and send loop runs in test mode at zero cost with identical call shapes, and only the final live transmission needs the live inbox. Collapse recipients, because one message to a shared alias is one unit where the same content to eight individuals is eight. And filter inbound at the source: every newsletter, bounce notification and automated receipt landing in an agent mailbox is a unit spent on something the agent will discard, which is a strong argument for keeping the agent’s address off distribution lists entirely, as described in how an agent inbox differs from a Gmail account.

If you hit the ceiling and want your data out, the export path still works while sending is refused. Exports are capped at two requests per hour per member, which returns 429 rate_limited rather than a quota error, and at 500 inboxes and 5,000 messages per export, and they produce JSON you keep. The inbox model everything above operates on is described on the agent inbox product page.


Keep reading

All guides · Documentation · Inboxes