test_mode_required
HTTP 403 retryable: false REST and MCP
The endpoint only exists for test projects, and the project or inbox you aimed it at is a live one.
Why it happens
Inbound simulation injects a message directly into a mailbox without it crossing a transport. Allowing that against a live inbox would mean live mail history could contain messages nobody sent, so the check runs twice: once on the project's environment and again on the specific inbox's environment, inside the same transaction that would write the message. The second check matters because a project and an inbox are separately recorded, and only the inbox row proves where the message would actually land.
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 project resolved from the path is not a test project. Checked before any inbox lookup.
packages/domain/src/services.ts:2123 - The inbox named in the body has environment live, even though the project passed the first check.
packages/domain/src/services.ts:2136
How to fix it
- Create a test project and a test inbox, then aim the simulation at those. Up to three active test inboxes are allowed per workspace.
- Check the key prefix. An efa_test_ key resolves only to test projects and test inboxes, so if your key is efa_live_ you are pointed at the wrong environment entirely.
- Do not look for a flag that relaxes this. Environment is carried in the key prefix and cannot be crossed, and a project's environment cannot be converted after creation.
- To exercise a live inbox end to end, send real mail to its address on agents.emailforagents.ai instead of simulating.
Why it works this way
There is no promotion path from a test project to a live one. That is a deliberate constraint rather than a missing feature: it keeps the environment of every message decidable from the row it is stored in, which is what lets a single check at the inbox level be trustworthy.
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}/test/inbound
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_mode_required",
"message": "Simulation endpoints only work on test projects.",
"request_id": "req_...",
"retryable": false
}
}
Verified against packages/domain/src/services.ts:2123. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one
place a code is mapped to a status.