Operations · Errors & rate limits

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:

400 Bad Requestapplication/json
{
  "error": {
    "type": "validation_error",
    "code": "invalid_request",
    "message": "Email address is malformed",
    "param": "to"
  }
}
FieldTypeDescription
errorrequiredobject
error.typerequiredvalidation_error | authentication_error | authorization_error | not_found | rate_limit | conflict | internal_error
error.coderequiredstringStable, machine-readable.
error.messagerequiredstringHuman-readable, may change.
error.paramstringThe offending input field, when known.
error.detailsobjectMore 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

StatusCodeWhen
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:

BucketLimit
default120 requests / minute
dns20 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:

FieldTypeDescription
X-RateLimit-LimitintegerRequests allowed per minute in this endpoint's bucket, for this key.
X-RateLimit-RemainingintegerRequests left in the current window.
X-RateLimit-ResetintegerUnix time (seconds) when the current window ends.
Retry-AfterintegerSeconds until the window resets, or until the daily quota does.
retry
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

ReplyWhenRetry
451At 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
452At RCPT TO: past the key's rate limit. After DATA: past the daily sending quota.yes, later
535At 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.8At AUTH: the key and username are right, but the username's domain is not verified yet.yes, once the domain is verified
550Validation, 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.1After 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.1At 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.4At 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
A mail server relaying to postfly (Postfix, Exim) retries 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.