test_limit_exceeded
HTTP 403 retryable: false REST and MCP
The action is valid but it would push a test-mode allowance past its cap, so it was refused before anything was written.
Why it happens
Test mode is free and therefore bounded, and the bounds are checked against a locked usage row rather than a cached count. Three separate caps produce this code: three active test inboxes per workspace, 100 retained test messages, and 10 MB of retained test content. The message and byte checks run together on inbound simulation, and the byte total includes the text, the HTML and every attachment, so a single large message can trip the cap on its own. Test content also expires after 7 days, which is why a workspace that was at the cap yesterday may be under it today without anyone deleting anything.
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 workspace already has 3 active test inboxes.
packages/domain/src/services.ts:1371 - Retained test messages are already at 100, or the incoming message's text, HTML and attachment bytes would take retained test bytes past 10 MB.
packages/domain/src/services.ts:1806 - A declared upload size would take retained test bytes past 10 MB. Checked at reservation, before any bytes are accepted.
packages/domain/src/services.ts:3563
How to fix it
- Delete a test inbox you are no longer using. The cap counts active inboxes, so a deleted one frees a slot.
- Wait for expiry if the cap is on content rather than inboxes. Test content expires after 7 days and the counters fall as it goes.
- Shrink the fixture. The byte check sums text, HTML and attachments, so trimming a large HTML body is usually faster than deleting messages.
- Do not create a second workspace to get more test capacity. The caps are per workspace and the free live allowance is per verified owner identity, so it does not renew.
Why it works this way
The storage check runs at upload reservation rather than at upload completion. Checking after the bytes arrive would mean accepting content the workspace has no room for and then deleting it, and two parallel uploads could each pass an unreserved check and together exceed the cap.
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}/inboxesPOST /projects/{project_id}/test/inboundPOST /projects/{project_id}/uploads
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 403
content-type: application/json
x-request-id: req_...
{
"error": {
"code": "test_limit_exceeded",
"message": "This workspace already has the maximum number of test inboxes.",
"request_id": "req_...",
"retryable": false
}
}
Verified against packages/domain/src/services.ts:1371. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one
place a code is mapped to a status.