not_found
HTTP 404 retryable: false REST and MCP
No resource matching the identifiers in the path is visible to this credential, which covers both a resource that does not exist and one that exists outside the credential's allowed projects or inboxes.
Why it happens
Two different situations deliberately share this code. A row lookup that returns nothing raises it, which is the ordinary case. So does a scope check: when a credential is restricted to specific projects or inboxes and the path names one outside that list, the guard raises not_found rather than permission_denied. From outside, a project you may not touch is indistinguishable from a project that was never created, which is the point.
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 credential is restricted to a project list and the project in the path is not on it.
packages/domain/src/authorize.ts:17 - The credential is restricted to an inbox list and the inbox in the path is not on it.
packages/domain/src/authorize.ts:27 - A row lookup that must return exactly one record returns none. This is the shared helper behind most single-resource reads.
packages/domain/src/services.ts:96 - An attachment id does not resolve within the workspace, project and inbox it was requested under.
packages/domain/src/outbound-files.ts:18 - The workspace's lifecycle status is deleted.
packages/domain/src/services.ts:917
How to fix it
- Check the identifier prefix. Ids are prefixed by type, so prj_, inb_ and msg_ are not interchangeable and a copied id from the wrong column fails as not_found.
- List the parent collection with the same credential. If the resource is absent from that listing, the credential is scoped away from it rather than the resource being missing.
- Check the credential's project and inbox restrictions at GET /me. A key created with an inbox list only ever sees those inboxes.
- Confirm the environment. A test key resolves only to test projects, so a live project id under a test key is correctly a 404.
Why it works this way
Returning 404 instead of 403 for an out-of-scope resource is a deliberate trade against debuggability. A 403 would confirm that the id you guessed names a real project in someone's workspace, which turns any endpoint taking an id into an existence oracle. The cost is that a scoping mistake looks like a typo, which is why GET /me reports the restrictions.
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.
GET /projects/{project_id}/inboxesGET /projects/{project_id}/inboxes/{inbox_id}/messages/{message_id}/bodyGET /me
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 404
content-type: application/json
x-request-id: req_...
{
"error": {
"code": "not_found",
"message": "Resource not found.",
"request_id": "req_...",
"retryable": false
}
}
Verified against packages/domain/src/authorize.ts:17. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one
place a code is mapped to a status.