Skip to content

Rate limits and quotas

Every request limit the API enforces, the headers that report it, what a 429 looks like, and the monthly and daily sending quotas that sit on top.

How request limits work#

Each rate-limited endpoint keeps a counter per credential, per method and exact path, over a fixed window:

  • Per credential. Requests are counted against the API key (or OAuth token) that made them — not your IP address, and not your team. Two keys each get their own allowance.
  • Per exact path. The path includes IDs, so POST /domains/dom_a…/verify and POST /domains/dom_b…/verify are counted separately, and so are POST /emails and its alias POST /emails/send.
  • Fixed window. The window starts with the first request and resets completely when it ends; it doesn't slide. Most windows are one minute.
  • Free plan runs at half rate. Teams on the Free plan (or with no subscription) get half of each limit, rounded down. Upgrading raises the limit within five minutes. If you need more than your plan gives, contact support — limits can be raised per team.

GET requests (lists and lookups) have no per-endpoint limit, except GET /contacts/export. Everything, reads included, is still subject to the edge limits.

Limits by endpoint#

Paths are relative to https://api.poststack.dev. Anything not listed has no per-endpoint limit.

Sending and content

EndpointPaid plansFree plan
POST /emails60 / minute30 / minute
POST /emails/send60 / minute30 / minute
POST /emails/batch10 / minute5 / minute
POST /emails/preview30 / minute15 / minute
POST /emails/spam-preview30 / minute15 / minute
POST /emails/:id/cancel30 / minute15 / minute
POST /email-validations60 / minute30 / minute
POST /email-validations/batch10 / minute5 / minute
POST /broadcasts30 / minute15 / minute
PATCH /broadcasts/:id30 / minute15 / minute
POST /broadcasts/:id/send10 / minute5 / minute
POST /broadcasts/:id/test10 / minute5 / minute
POST /broadcasts/:id/resend10 / minute5 / minute
POST /broadcasts/:id/cancel10 / minute5 / minute
POST /broadcasts/:id/end-ab-test10 / minute5 / minute
POST /templates30 / minute15 / minute
POST /templates/presets/:presetId/use30 / minute15 / minute
PATCH /templates/:id30 / minute15 / minute
DELETE /templates/:id30 / minute15 / minute
POST /templates/:id/duplicate30 / minute15 / minute
POST /templates/:id/render60 / minute30 / minute
POST /templates/:id/publish30 / minute15 / minute
POST /templates/:id/unpublish30 / minute15 / minute
POST /workflows30 / minute15 / minute
PUT /workflows/:id/graph60 / minute30 / minute
POST /workflows/:id/activate30 / minute15 / minute
POST /workflows/:id/pause30 / minute15 / minute
POST /workflows/:id/trigger10 / minute5 / minute
POST /workflows/events300 / minute150 / minute
POST /inbound/:id/reply60 / minute30 / minute
POST /inbound/:id/forward60 / minute30 / minute
POST /inbound/:id/draft-reply60 / minute30 / minute

Account, audience and configuration

EndpointPaid plansFree plan
POST /domains10 / minute5 / minute
POST /domains/:id/verify10 / 15 minutes5 / 15 minutes
POST /domains/:id/dkim/rotate5 / 1 hour2 / 1 hour
POST /domains/:id/dkim/rotate/activate20 / 15 minutes10 / 15 minutes
DELETE /domains/:id/dkim/rotate10 / 1 hour5 / 1 hour
POST /domains/:id/ip10 / minute5 / minute
DELETE /domains/:id/ip10 / minute5 / minute
POST /api-keys10 / minute5 / minute
POST /api-keys/:id/rotate10 / minute5 / minute
DELETE /api-keys/:id10 / minute5 / minute
POST /webhooks10 / minute5 / minute
PATCH /webhooks/:id10 / minute5 / minute
DELETE /webhooks/:id10 / minute5 / minute
POST /webhooks/:id/test10 / minute5 / minute
POST /webhooks/:id/rotate-secret10 / minute5 / minute
POST /webhooks/:id/deliveries/:did/replay10 / minute5 / minute
POST /webhooks/:id/deliveries/batch-replay3 / minute1 / minute
POST /notification-channels20 / minute10 / minute
PUT /notification-channels/:id60 / minute30 / minute
DELETE /notification-channels/:id60 / minute30 / minute
POST /notification-channels/:id/test10 / minute5 / minute
POST /notification-channels/:id/deliveries/:deliveryId/replay10 / minute5 / minute
POST /contacts60 / minute30 / minute
POST /contacts/import10 / minute5 / minute
GET /contacts/export5 / minute2 / minute
POST /segments30 / minute15 / minute
POST /segments/preview10 / minute5 / minute
POST /contact-properties30 / minute15 / minute
PATCH /contact-properties/:id30 / minute15 / minute
DELETE /contact-properties/:id30 / minute15 / minute
POST /subscription-topics30 / minute15 / minute
PATCH /subscription-topics/:id30 / minute15 / minute
DELETE /subscription-topics/:id30 / minute15 / minute
POST /subscription-topics/contacts/:contactId/subscriptions30 / minute15 / minute
DELETE /subscription-topics/contacts/:contactId/subscriptions/:topicId30 / minute15 / minute
POST /suppressions30 / minute15 / minute
POST /suppressions/import10 / minute5 / minute
DELETE /suppressions/:email30 / minute15 / minute
POST /signup-forms30 / minute15 / minute
PATCH /signup-forms/:id30 / minute15 / minute
DELETE /signup-forms/:id30 / minute15 / minute
POST /signup-forms/:id/submit10 / minute10 / minute per IP address
POST /mailboxes10 / minute5 / minute
PATCH /mailboxes/:id10 / minute5 / minute
DELETE /mailboxes/:id10 / minute5 / minute
POST /mailboxes/:id/password10 / minute5 / minute
POST /mailboxes/:id/shares10 / minute5 / minute
DELETE /mailboxes/:id/shares/:targetId10 / minute5 / minute
PUT /mailboxes/:id/filters10 / minute5 / minute
POST /mailboxes/aliases10 / minute5 / minute
DELETE /mailboxes/aliases/:id10 / minute5 / minute
ANY /mcp120 / minute60 / minute

POST /signup-forms/:id/submit is the public endpoint your website's form posts to, so it is counted per visitor IP address and is the same on every plan. /mcp is one counter for every MCP tool call made with that key.

Sending more than 60 emails a minute

Use POST /emails/batch: up to 100 emails per request at 10 requests a minute is 1,000 emails a minute per key on a paid plan, against 60 for single sends.

Rate-limit headers#

Every response from a rate-limited endpoint carries three headers, whether or not the limit was hit:

bash
HTTP/1.1 202 Accepted
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1790676120
X-RateLimit-Limitinteger
Requests allowed in the current window for this key and endpoint — already halved on the Free plan.
X-RateLimit-Remaininginteger
Requests left in the window. 0 means the next one will be refused.
X-RateLimit-Resetinteger
When the window ends, as Unix time in seconds.
Retry-Afterinteger
Sent only on a rate-limit 429: seconds until the window ends.

Endpoints with no per-endpoint limit (most GETs) don't send these headers.

The 429 response#

bash
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790676120
Retry-After: 38

{
  "error": "Rate limit exceeded. Please try again later.",
  "code": "rate_limit_exceeded"
}

The request was not processed — nothing was sent or changed — so it is always safe to repeat it after Retry-After seconds. Requests refused this way still count toward the window, so retrying in a tight loop keeps you limited until the window ends.

A 429 can also mean a sending quota was reached. Each has its own code and no Retry-After, and retrying it soon won't help. Branch on code, not on the error text:

codeCauseWhen it clears
rate_limit_exceededRequest rate limit on this endpoint.After Retry-After seconds.
monthly_limit_exceededFree plan used its 3,000 emails for the month.The 1st of next month (UTC), or on upgrade.
daily_limit_exceededThe external-recipient cap for new and Free accounts (the error states the cap that fired).24 hours after the first counted send of the window.
send_limit_exceededSending from new accounts is paused platform-wide (a safety brake).Automatically; established accounts are unaffected.

In a POST /emails/batch response, an element refused by a quota carries the same code next to its error.

Over SMTP the same conditions come back as 451 (temporary), so a mail client or MTA retries on its own schedule.

Retrying#

  • Honour Retry-After. Wait that many seconds, then send again. Add a little random jitter if many workers share a key so they don't all return in the same second.
  • Pace instead of bursting. Spreading 60 sends across the minute never trips the limit; sending 60 in the first second and then waiting works too, but only if you actually wait.
  • Tell a rate limit from a quota. Only rate_limit_exceeded has Retry-After. For the quota codes above, stop and alert a person.
  • Make sends retry-safe. Put an idempotency_key on every send so a retry can never deliver twice — see Idempotency.

The TypeScript, Python and Go SDKs retry 408, 429 and 5xx responses up to 3 times by default, with full-jitter backoff starting at 250 ms. A rate-limit 429 waits its Retry-After instead (up to 60 seconds; longer is raised to your code with the wait on the error). A quota 429 — any of the codes above — is raised at once, never retried:

# -D - prints the response headers; read Retry-After from a 429
curl -sS -D - -o /dev/null -X POST https://api.poststack.dev/emails \
  -H "Authorization: Bearer $POSTSTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d @email.json

Edge limits#

In front of the API, the load balancer limits each client IP address, whatever credential it uses:

PathsSustained rateBurst
POST /emails/send, POST /emails/batch10 requests / second20
Every other API path30 requests / second50

A request refused here gets a 429 with a small HTML body instead of JSON, and no rate-limit headers. Back off for a second or two. You are only likely to see this from many parallel workers behind one IP.

Request bodies over 36 MB are refused at the edge with 413. That ceiling sits above the attachment limits below so that a message within them is never cut off by it.

Sending quotas#

Monthly emails

Every accepted message counts once toward the calendar month (UTC), however many recipients it has. A batch of 100 counts 100. Test-mode sends and idempotent replays don't count.

PlanIncluded per monthPast the allowance
Free3,000Sends are refused with 429 until the month ends
Starter10,000Keeps sending; billed at €0.25 per 1,000
Pro50,000Keeps sending; billed at €0.25 per 1,000
Scale100,000Keeps sending; billed at €0.25 per 1,000
EnterpriseUnlimited—

Current usage is on the Settings → Billing page.

New-account external-recipient cap

To protect the shared sending IPs, a send with at least one recipient outside the domains your team has added counts against a daily cap. One send is one unit, no matter how many external recipients it has; it applies per team across all keys and SMTP, and test-mode sends are exempt.

WhoCapWhat happens past it
Any account in its first hour10 external sendsSending is put on hold for a safety review (403); contact support to lift it.
Free plan, and paid plans in their first 72 hours50 external sends per 24 hours429 for the rest of the window. In the account’s first 24 hours the first breach puts sending on hold for review (403) instead.
Paid plans after 72 hoursNo fixed capA send far above anything the account has sent before is held for review (403).

The 24-hour window is fixed: it starts at the first counted send and the whole count resets when it ends.

Past the cap the 429 carries code: "daily_limit_exceeded" and its message states the cap that fired (10 in the first hour, 50 after).

SMTP mailboxes

Mail submitted over SMTP with a mailbox login (not an API key) is limited to 60 messages per hour per mailbox; past that the relay answers Rate limit exceeded. Try again later. Ten failed SMTP logins from one IP address or for one username within 15 minutes block further attempts for the rest of that window. SMTP sends authenticated with an API key have no per-message rate limit but count toward the same quotas as the API.

Account limits#

ResourceFreeStarterProScaleEnterprisePast it
Domains13101,000Unlimited403 Domain limit reached for … plan
API keys510501,000Unlimited403 API key limit reached for … plan
Webhooks2020202020403 Webhook limit reached (20 per team)
Email log retention14 days14 days30 days90 daysUnlimitedOlder emails are deleted

Per-request limits#

Limits on the size of a single send. Schema limits answer 400; the checks that run after validation answer 422. Both name the limit in error.

LimitValueStatus
Recipients per email (to + cc + bcc)50 in total422
Emails per batch request100400
Attachments per email10400
Size of one attachment (decoded)10 MB422
Total attachments per email (decoded)25 MB422
Tags per email10, each up to 64 characters400
Subject998 characters400
Custom header value1,024 characters400
Contacts per import request10,000400

Troubleshooting#

I get 429 on the first request of the day

Check the code. If it isn't rate_limit_exceeded, it is a quota — most often monthly_limit_exceeded (the Free plan's 3,000) or daily_limit_exceeded (the external-recipient cap) — and the table under The 429 response says when it clears.

X-RateLimit-Limit is half of what this page says

Your team is on the Free plan, or has no subscription. Paid plans get the full figure within five minutes of upgrading.

I got a 429 with an HTML body

That came from the edge limit on your IP address, not from the API. Reduce parallelism from that host, or spread workers across the minute.

Next steps

Was this page helpful?

Related