Errors
Every error response carries a structured envelope with a machine-readable code from a closed catalog, so your client branches on code instead of parsing messages. Each entry also carries a human message, an actionable suggestion, and a docs link straight to the relevant row below.
The envelope
Requests authenticated with a scoped efa_ API key receive the envelope as the entire response body, with the canonical HTTP status from the catalog below. Requests authenticated with the account-wide legacy token keep their original status codes and body fields exactly as they always were — the envelope is added under a top-level error key, so existing integrations never break.
# Scoped key (efa_...) — the envelope is the body
{
"code": "VALIDATION_ERROR",
"type": "invalid_request_error",
"message": "The request failed validation.",
"suggestion": "Fix the fields listed in details and retry the request.",
"docs": "https://emailflow.ai/docs/api-errors#errors-validation-error",
"param": "from_email",
"details": { "from_email": ["The from email must be a valid email address."] }
}
# Legacy token — original body, envelope added under "error"
{
"from_email": ["The from email must be a valid email address."],
"error": { "code": "VALIDATION_ERROR", "type": "invalid_request_error", ... }
}
Optional fields appear only when they apply: param names the offending input, details carries field-keyed validation messages or structured context, and retryAfter (on RATE_LIMITED) is the number of seconds to wait, matching the Retry-After header.
The catalog
The full closed catalog — statuses shown are the canonical ones returned to scoped keys:
| Code | Status | Type | What to do |
|---|---|---|---|
INVALID_REQUEST |
400 | invalid_request_error |
Check the request method, path, and parameters against the API reference. |
INVALID_API_KEY |
401 | authentication_error |
Create a new key under Account -> API, or check that the key was copied in full. |
INSUFFICIENT_PERMISSIONS |
403 | permission_error |
Use a key whose scopes cover this endpoint, or create one under Account -> API. |
NOT_FOUND |
404 | invalid_request_error |
Check the identifier; the resource may have been deleted or may belong to another account. |
VALIDATION_ERROR |
422 | invalid_request_error |
Fix the fields listed in details and retry the request. |
RATE_LIMITED |
429 | rate_limit_error |
Slow down and retry after the number of seconds given in retryAfter. |
IDEMPOTENCY_CONFLICT |
409 | invalid_request_error |
Use a fresh Idempotency-Key for a different request body, or replay the identical body. |
CREDITS_EXHAUSTED |
402 | billing_error |
Upgrade your plan or wait for the next billing cycle to refresh your credits. |
PLAN_LIMIT |
402 | billing_error |
Upgrade your plan to raise this limit. |
CONFLICT |
409 | invalid_request_error |
The resource is in a state that does not allow this operation; fetch it again and check its status. |
SERVER_ERROR |
500 | api_error |
Retry later; if the problem persists, contact support with the request timestamp. |
Validation errors
When input fails validation, scoped keys receive 422 VALIDATION_ERROR with field-keyed messages under details (the first offending field is also named in param). Legacy-token callers keep the original 403 with the bare field-keyed map as the body:
# Legacy token — original validation shape (403)
{
"from_email": [
"The from email must be a valid email address."
]
}
Handling errors in practice
Branch on code, not on message — messages may be reworded; codes never change (the catalog only grows). The cases worth handling explicitly:
| Code | Client behavior |
|---|---|
RATE_LIMITED | Wait retryAfter seconds, then retry. The TypeScript SDK does this automatically. |
IDEMPOTENCY_CONFLICT | With details.reason: "in_flight", the first attempt is still running — wait and retry. Otherwise you reused a key with a different body — pick a fresh key. |
CREDITS_EXHAUSTED / PLAN_LIMIT | Not retryable. Surface to a human: top up credits or raise the quota under plans & quotas. |
SERVER_ERROR | Safe to retry with the same Idempotency-Key — failed attempts are never stored, so the retry executes normally. |