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, resetAt and scope (domain, container or customer: the limit that refused) 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. |
| 400 | invalid_json | The body is not JSON. |
| 400 | unknown_field | The body or query has a field or parameter postfly does not support; it is refused, not ignored. |
| 400 | attachment_invalid_base64 attachment_filename_invalid | An attachment is not clean base64, or its file name is empty once cleaned. |
| 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 | Sending 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 domain limit is reached. |
| 403 | global_scope_forbidden global_suppression suppression_limit_reached | Suppression rules. |
| 404 | message_not_found domain_not_found suppression_not_found | Not one this key can see. |
| 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 your list or the global one. |
| 409 | domain_taken | The domain is registered already, here or by another customer. |
| 413 | payload_too_large attachment_too_large message_too_large | The body, an attachment or the whole message is over its size limit. |
| 422 | attachment_type_not_allowed attachment_extension_blocked attachment_extension_mismatch attachment_inline_not_supported too_many_attachments | An attachment has a content type or file extension postfly does not send, a file name that does not match its type, or there are too many. |
| 429 | rate_limit_exceeded | The key used up its rate-limit bucket. |
| 429 | daily_quota_exceeded | Today's sending cap is reached. |
| 429 | send_concurrency_exceeded | Too many large sends are being processed at once; retry after Retry-After. |
| 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 group of domains.
Daily sending quota
A group 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, resetAt and scope: domain, container or customer, whichever limit refused. The SMTP 452 names it too. 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. After DATA: too many messages are being received at once. Also any temporary server fault. | yes, later |
| 452 | At RCPT TO: past the key's rate limit. After DATA: past the daily sending quota. | yes, later |
| 535 | At AUTH: the key is wrong, revoked or paused, lacks emails:send, or the username names another key or a domain the key may not send from. The reply does not say which. | no, fix the credentials |
| 535 5.7.8 | At AUTH: the key and username are right, but the username's domain is not verified yet. | yes, once the domain is verified |
| 550 | Validation, sender domain or suppression problem (for any recipient: then none is queued); too many recipients; a Reply-To with several addresses. | no |
| 550 5.6.1 | After DATA: an attachment postfly does not carry — a type outside the allowlist (calendar invitations and forwarded messages included) or an image embedded in the HTML. | no |
| 550 5.7.1 | At MAIL FROM: sending is paused. After DATA: an attachment with a blocked file extension, or a file name that does not match its type. | no (paused: until resumed) |
| 552 5.3.4 | At MAIL FROM: the declared SIZE is over the limit. After DATA: the message, an attachment, the number of attachments, or the attachments of all recipients together is over its limit. | no |
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.