Error reference

policy_denied

HTTP 403 retryable: false REST and MCP

Nothing is wrong with the credential or the request; the inbox's own sending policy evaluated this message and returned a deny decision.

Why it happens

Policy evaluation is a separate stage from permission. It runs against the canonical payload and returns allow, approval or deny, and the response carries the reason codes in details.reason_codes so you can tell which rule fired. The policy is also re-evaluated at commit time, not only at submission: a send that was approved and then waits is re-checked against the policy in force when it commits, and an approval decision that no longer satisfies current policy is refused rather than honoured. The daily recipient cap lands here too, which is worth knowing because the contract declares a separate daily_limit_exceeded code that nothing throws.

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.

How to fix it

  1. Read details.reason_codes on the response. It names the rules that fired, which is the only way to tell a recipient block from a daily cap from an allow-list miss.
  2. Check the inbox policy at GET /projects/{project_id}/inboxes/{inbox_id}/policy. Recipient allow rules, recipient block rules and the daily recipient limit all live there.
  3. If the rule was require-approval rather than a block, submit a draft and let a human release it instead of retrying the direct send. A send carrying no human approval is refused when policy asks for one.
  4. Do not retry a daily-cap denial on a short backoff. The cap resets on its own schedule and this code is not retryable.

Why it works this way

Re-evaluating at commit rather than trusting the submission decision is what makes approval.invalidated meaningful. An approval granted under one policy does not carry authority into a stricter one, so tightening a policy takes effect on sends that are already in flight.

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.

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": "policy_denied",
    "message": "Workspace policy blocked this send.",
    "request_id": "req_...",
    "retryable": false
  }
}

Verified against packages/domain/src/services.ts:1546. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one place a code is mapped to a status.

All error codes · Documentation · Quickstart