Errors & rate limits
The error envelope and codes, per-key rate limits, the daily sending quota with Retry-After, and the SMTP replies that go with them.
Error envelope
Every 4xx and 5xx answer of the API has the same shape:
{
"error": {
"type": "validation_error",
"code": "invalid_request",
"message": "Email address is malformed",
"param": "to"
}
}| Field | Type | Description |
|---|---|---|
| errorrequired | object | |
| error.typerequired | validation_error | authentication_error | authorization_error | not_found | rate_limit | conflict | internal_error | |
| error.coderequired | string | Stable, machine-readable. |
| error.messagerequired | string | Human-readable, may change. |
| error.param | string | The offending input field, when known. |
| error.details | object | More about some errors: limit, used and resetAt for daily_quota_exceeded. |
type is one of validation_error, authentication_error, authorization_error, not_found, rate_limit, conflict, internal_error. Branch on code: it is stable, while message is for people and may change.
Error codes
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body or query failed validation (unknown query parameters included). |
| 400 | invalid_json | The body is not JSON. |
| 401 | missing_authorization invalid_api_key_format invalid_api_key api_key_revoked api_key_paused | Authentication failed. |
| 403 | insufficient_scope | The key lacks the scope the endpoint needs. |
| 403 | project_paused | The project is paused; nothing new is accepted. |
| 403 | sender_domain_restricted | The key is restricted to another sender domain. |
| 403 | sender_domain_not_registered sender_domain_not_verified | The from domain cannot send yet. |
| 403 | domain_reserved domain_limit_reached | postfly's own domain, or the project's domain limit. |
| 403 | global_scope_forbidden global_suppression suppression_limit_reached | Suppression rules. |
| 404 | message_not_found domain_not_found suppression_not_found | Not in this project. |
| 404 | route_not_found | No such /v1 endpoint. |
| 405 | method_not_allowed | Wrong method; the Allow header lists the right ones. |
| 409 | recipient_suppressed | The recipient is on the project or the global suppression list. |
| 409 | domain_taken | The domain is registered already, in this or another project. |
| 429 | rate_limit_exceeded | The key used up its rate-limit bucket. |
| 429 | daily_quota_exceeded | The project reached today's sending cap. |
| 500 | internal_error | Our fault. |
Which endpoint returns which is in the API reference.
Rate limits
Limits count per API key, in fixed one-minute windows. Each endpoint counts against one bucket:
| Bucket | Limit |
|---|---|
| default | 120 requests / minute |
| dns | 20 requests / minute |
The dns bucket is for the endpoints that make DNS lookups for you. Every authenticated response carries the state of its bucket; over the limit the answer is 429 rate_limit_exceeded with Retry-After:
| Field | Type | Description |
|---|---|---|
| X-RateLimit-Limit | integer | Requests allowed per minute in this endpoint's bucket, for this key. |
| X-RateLimit-Remaining | integer | Requests left in the current window. |
| X-RateLimit-Reset | integer | Unix time (seconds) when the current window ends. |
| Retry-After | integer | Seconds until the window resets, or until the daily quota does. |
const res = await fetch(url, init);
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? 1);
await new Promise((r) => setTimeout(r, wait * 1000));
// …then retry the request
}Other bounds: at most 50 sender domains and 100,000 suppressions per project.
Daily sending quota
A project on a warmup schedule or with a daily cap (see Warmup & daily caps) gets 429 daily_quota_exceeded from POST /v1/emails once today's messages reach the cap. Retry-After counts the seconds to midnight Europe/Bratislava time, when the count starts over, and error.details carries limit, used and resetAt. Retrying sooner doesn't help; queue the message on your side.
SMTP replies
| Reply | When | Retry |
|---|---|---|
| 451 | At MAIL FROM: the key's rate limit is used up. Also any temporary server fault. | yes, later |
| 452 | At RCPT TO: past the key's rate limit. After DATA: past the project's daily quota. | yes, later |
| 550 | Validation, sender domain or suppression problem; too many recipients. | no |
| 550 5.7.1 | At MAIL FROM: the project is paused. | no, until resumed |
4xx replies by itself. A library that talks SMTP from your app, such as Nodemailer, reports them as errors: retry those later yourself. A 5xx is final. More in Replies & limits.