Errors, retries and limits
Error envelope
Every non-2xx JSON response has the same shape:
{
"error": {
"code": "quota_exceeded",
"message": "daily quota of 5000 recipients exceeded"
}
}
code is stable and meant for programs; message is for humans and may change.
Status codes
| Status | Meaning | What to do |
|---|---|---|
400 |
Malformed request, invalid parameter or unsupported operation | Fix the request |
401 |
Missing, invalid, expired or revoked credential | Check the key; rotate if leaked |
403 |
Missing scope, suspended scope, recipient not on the inbox allowlist, free-trial restriction, or submission refused by a reject pause |
Do not retry blindly; inspect code |
413 |
Request body or attachments too large | Reduce the payload |
404 |
Unknown resource, or one outside your tenant / inbox scope | Treat as not found |
409 |
State conflict (inbox_paused, approval already decided, duplicate folder…) |
Read current state and reconcile |
422 |
Valid JSON but invalid send data (bad address, missing body, oversize attachment) | Fix the message |
429 |
Quota or rate limit exceeded | Wait for Retry-After seconds, then retry |
451 |
Request hit the wrong data-region endpoint | Use the region your account is pinned to |
503 |
Temporary unavailability or a delivery dependency is down | Retry with back-off; honour Retry-After if present |
Common error codes
| Code | Status | Notes |
|---|---|---|
invalid_request |
400 | Parameter or body problem; message names the field |
invalid_folder |
400 | Folder name violates the naming rules |
unauthorized, key_expired, invalid_credentials |
401 | Bad, missing or expired bearer / login |
forbidden |
403 | Key lacks the scope, or the resource is not manageable by this key |
ip_not_allowed |
403 | Request source is outside the key’s allowed_cidrs |
account_suspended, tenant_disabled, sending_suspended |
403 | A pause or disablement covers this submission |
recipient_not_allowed |
403 | Inbox allow_recipients policy |
trial_domain, trial_recipient |
403 | Free-trial account used a non-shared domain or a recipient other than its own verified address |
not_found |
404 | Unknown or out-of-scope resource |
inbox_paused |
409 | Inbox status is paused |
already_decided |
409 | Approval was already approved or rejected |
mx_unverified, inbound_disabled |
409 | Domain cannot receive yet |
folder_not_empty |
409 | Delete a non-empty custom folder without ?move_to= |
unsafe_content |
409 | Outbound body failed a hard Shield block |
too_large |
413 | Request or attachments exceed 25 MiB |
too_many_recipients, invalid |
422 | Send data rejected by the submission pipeline |
folder_limit |
422 | More than 100 custom folders |
quota_exceeded, rate_limited, throttled |
429 | Tenant quota or rate; Retry-After is set |
trial_daily_cap |
429 | Free trial: 50 sends/day |
too_many_streams |
429 | More than 5 concurrent WebSocket streams on one key |
wrong_region |
451 | Account is pinned to another data region |
unavailable, spool_unavailable, approval_unavailable, payments_unavailable, ai_unavailable |
503 | Temporary; retry with back-off |
Retrying safely
- Sends: always set
Idempotency-Key. Then any429/503/network timeout is safe to retry: a replay returns the originalmessage_idwithduplicate: true. - Reads: idempotent by nature; retry with back-off.
- Mailbox actions (
move,read, labels, batch): idempotent; repeating them is harmless. - Compose / reply / forward from an inbox: not idempotent today. On an ambiguous failure, list
folder=sentfor the thread before retrying. - Webhooks you receive: at-least-once; deduplicate on
X-Cyfox-Event-Id.
Limits
| Limit | Value |
|---|---|
| Attachments per message | 25 MiB total after base64 decoding |
| Recipients per message | Tenant max_recipients_per_message |
| Daily / monthly recipients, per-minute / per-hour rates | Tenant quotas; 429 + Retry-After when exceeded |
| List page size | default 50, maximum 200 (events: default 100, max 200) |
| Custom folders per inbox | 100; names 1–64 characters |
| Batch mailbox action | 1–200 message IDs |
| Concurrent WebSocket streams | 5 per key |
| Webhook timestamp tolerance | 5 minutes |
| AI extraction | Monthly token cap per account |
| Free-trial accounts | 50 sends/day, own verified address only, shared domain |
Data regions
Accounts are pinned to a data region at signup. Requests sent to another region’s endpoint receive 451 and are not processed. Enterprise customers can choose the region; others are placed in the default region.
Health endpoints
GET /healthz returns 200 while the process is up; GET /readyz returns 503 when the database or pause cache is unavailable. Current status for the hosted service is published on the status page linked from the console.