quota_exceeded
HTTP 402 retryable: false REST and MCP
The workspace holds the entitlement for this action, but the metered allowance behind it has no headroom left for the current period.
Why it happens
Quota is reserved, not counted after the fact. The send path runs a conditional UPDATE that only succeeds while used + reserved + this request stays within allowance + additional_allowance, and the absence of an updated row is what raises this error. That is why two concurrent sends cannot both slip past a check-then-write race: the database decides, atomically, and the loser gets this code. An email unit is one message received or one recipient sent to, so a send to three people consumes three.
Every condition below was read in the implementation at the file and line given. They are the branches that raise this code, not a summary of what the name suggests.
- The atomic quota reservation for a send finds no headroom in the current period. The default go-live allowance is 100 monthly deliveries.
packages/domain/src/billing.ts:773 - Requesting an export while the workspace already retains 256 MiB of export objects that have not been purged.
packages/domain/src/exports.ts:193 - An export would cover more than 500 inboxes.
packages/domain/src/exports.ts:229 - An export would cover more than 5,000 messages. No partial export is created.
packages/domain/src/exports.ts:242 - Attaching a custom domain beyond the plan's custom-domain count.
packages/domain/src/custom-domains.ts:230
How to fix it
- Read GET /billing/entitlement for the remaining allowance rather than inferring it from your own send count. Inbound mail consumes units too.
- Count recipients, not messages. One send to three addresses is three units, and the deduplicated union of to, cc and bcc is what gets charged.
- Move development traffic to a test project. Test mail is never charged against the allowance.
- For an oversized export, narrow it by inbox or by date range. The limits are 500 inboxes and 5,000 messages, and exceeding either produces no partial artefact to salvage.
- For export storage, wait for expired exports to be purged automatically, then request again.
Why it works this way
Inbound is not symmetric with outbound here. Provider-accepted mail is committed to the workspace even beyond the allowance rather than being dropped for a billing reason, so exhausting quota stops sending and never discards mail that has already arrived.
Messages you will see
The message field is written for a human reading a log and is not a contract
value. Branch on code instead. These are the exact strings this code is sent
with today, listed so that a search for one of them lands here.
Where it appears
These are the documented surfaces that raise it. The REST base is https://api.emailforagents.ai/v1, and the remote MCP endpoint is https://mcp.emailforagents.ai/mcp.
POST /projects/{project_id}/inboxes/{inbox_id}/messagesPOST /projects/{project_id}/exportsGET /billing/entitlement
The response
Read request_id off the body or the x-request-id header before
doing anything else. It is the only handle on one specific attempt, and for a 503 it is
the only handle at all, because the underlying detail stays server-side.
HTTP/1.1 402
content-type: application/json
x-request-id: req_...
{
"error": {
"code": "quota_exceeded",
"message": "This workspace has exhausted its delivery allowance.",
"request_id": "req_...",
"retryable": false
}
}
Verified against packages/domain/src/billing.ts:773. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one
place a code is mapped to a status.