Handling errors
API errors use a JSON envelope with a stable type, a human-readable message and a request_id:
{
"status": 422,
"type": "invalid_from_address",
"message": "The 'from' address is not a valid mailbox on a domain you own",
"request_id": "req_01J8K2P...",
"doc_url": "https://docs.epostix.com/errors/invalid_from_address",
"details": []
}
A network failure or response from an intermediary may have no API envelope. Handle those separately from a structured rejection. The error catalogue describes each API type.
Use SDK errors
Official SDKs decode the envelope and expose typed errors. Calls throw on failure, or return an error in Go. The quickstarts include error handling for Node.js, Python, Go and PHP.
Branch on type, not the wording of message. Keep a fallback for unknown types. Log the request ID when present, alongside your own job or event ID, without logging API keys.
Field errors
When individual fields fail, details can include their paths and error codes:
{
"type": "validation_error",
"details": [
{"field": "from", "message": "property \"from\" is missing", "code": "required"},
{"field": "to", "message": "must contain at least one recipient", "code": "invalid"}
]
}
Do not assume details is populated on every failure. Show the general error when no field detail is available.
Retry decisions
| Failure | Action |
|---|---|
| Authentication, permission or validation error | Correct the request or credentials before trying again |
rate_limit_exceeded | Respect Retry-After and reduce request pressure |
idempotency_in_progress | Wait for Retry-After, then repeat the identical request |
daily_quota_exceeded | Wait for the daily reset or change the plan; do not hold an HTTP request open until then |
monthly_quota_exceeded | Check plan capacity or the billing-period reset; a contact limit may need audience cleanup |
| 5xx or transport failure | The outcome may be uncertain; retain the original idempotency key and use bounded backoff |
SDKs already retry eligible transient failures within a deadline. They do not automatically retry daily or monthly quota failures. Adding an unbounded retry loop around the SDK can hide a persistent problem.
Keep uncertain sends on the original key
A timeout does not tell you whether the server accepted the message. Reuse the original idempotency key, body and API key when retrying an uncertain send within the 24-hour window. A new key can create another email.
A definitively rejected request is different. A 4xx response can be cached by the idempotency handler, so after correcting a validation error use a new key for the corrected request. A 5xx is not cached and can follow partial work; inspect the message state when needed rather than treating retries as an exactly-once guarantee.