Skip to main content

Errors

Every failed request returns the same JSON body. Only type and details change.

{
"status": 422,
"type": "invalid_schedule",
"message": "scheduled_at must be within 72 hours",
"request_id": "req_01J8K2P...",
"doc_url": "https://docs.epostix.com/errors/invalid_schedule",
"details": []
}

Branch on type, never on message. Messages are written for people and are rewritten whenever the wording can be improved. Types are part of the contract and only ever get added to, so treat an unrecognised type as a generic failure rather than crashing on it.

request_id identifies the request in our logs. Quote it in a support ticket and we can find the exact call. doc_url is the page for that type on this site.

details is populated when a specific field is at fault. Each entry names the field, a message, and a code you can match on.

{
"details": [
{"field": "from", "message": "property \"from\" is missing", "code": "required"}
]
}

Deciding whether to retry

Three groups behave differently, and the difference matters more than the status code.

Never retry the same request. The request is wrong and will stay wrong. Fix it and send a new one. Everything in the 4xx range except the four below is in this group.

Retry after waiting. rate_limit_exceeded, daily_quota_exceeded and idempotency_in_progress all carry a Retry-After header in seconds. Wait that long, then send the identical request again.

Retry with backoff. internal_error and service_unavailable are ours, not yours. Retry with exponential backoff. If you sent an Idempotency-Key, reuse the same key so a request that actually succeeded is not sent twice.

The catalogue

TypeStatusMeaning
invalid_request400The body is not parseable JSON
authentication_failed401The API key is missing, malformed or unknown
api_key_expired401The key passed its expiry date
monthly_quota_exceeded402The monthly send allowance is used up
insufficient_scope403The key lacks the permission this route needs
api_key_ip_restricted403The key is not allowed from your address
domain_scope_restricted403The key is limited to other domains
domain_not_verified403The sending domain is not verified
workspace_suspended403The workspace is suspended
test_mode_restricted403A test key attempted a live-only operation
not_found404No such endpoint, or no such record
domain_not_found404The domain is unknown to this workspace
template_not_found404The template is unknown to this workspace
attachment_not_found404The attachment is unknown to this workspace
method_not_allowed405Wrong HTTP method for this path
email_not_cancellable409The message has already left this stage
idempotency_conflict409The key was reused with a different body
idempotency_in_progress409A request with this key is still running
api_key_in_use409You tried to delete the key you are using
payload_too_large413The request body is over the size limit
attachment_too_large413The attachments exceed the total size limit
unsupported_media_type415Content-Type is not application/json
validation_error422The body parsed but a field is invalid
invalid_from_address422The from address is not a usable mailbox
invalid_schedule422scheduled_at is in the past or too far ahead
duplicate_detected422The same message was sent repeatedly
batch_too_large422The batch is outside the allowed size
template_render_failed422The template could not be rendered
attachment_expired422The pre-uploaded attachment has expired
attachment_fetch_failed422An attachment URL could not be read
webhook_limit_reached422The workspace has all the endpoints it may have
webhook_url_not_allowed422The webhook URL is not a public address
rate_limit_exceeded429Too many requests in the current window
daily_quota_exceeded429The daily send allowance is used up
internal_error500Something failed on our side
service_unavailable503The service is restarting