API Documentation
Rate limits
The REST API uses a token bucket rate limiter with per-key and per-endpoint quotas. This page documents every limit, how to read the rate-limit headers, the correct way to handle 429 responses, and the implementation mistakes that turn a transient throttle into a thundering herd.
Why we rate-limit
Rate limits are a contract, not an obstacle. They protect the API infrastructure from a single misbehaving client (whether intentionally abusive or accidentally runaway), they ensure predictable latency for every customer on shared infrastructure, and they create a clear ceiling that lets you plan capacity without surprises. The limits documented below are calibrated for production senders — if your application reaches them under normal load, the right answer is usually to architect around them (queueing, batching, caching) rather than to retry harder.
Three categories of misuse account for the majority of 429 responses we see in practice. First, polling loops — an application checking message status every second instead of subscribing to webhooks. Second, infinite retries on already-failed requests — a 4xx error that is not 429 will not be fixed by retrying it. Third, parallel sends without coordination — ten worker processes each making 100 requests per second, with no awareness that they share a single API key's quota. The patterns at the bottom of this page exist to help you avoid those three.
Token bucket model
The rate limiter implements a token bucket algorithm rather than a fixed-window counter. The behavioural difference matters: with a fixed window, you can send 100 requests at 11:59:59 and another 100 at 12:00:00, and both pass — effectively 200 requests in two seconds. With a token bucket, the bucket refills at a steady rate and the burst is bounded by the bucket capacity, regardless of where you sit relative to a reset boundary.
• Each API key has a bucket per endpoint with a fixed capacity (the burst limit) and a steady refill rate (tokens per second).
• Each request consumes one token from the appropriate bucket.
• The bucket refills continuously at the steady-state rate. A bucket can never exceed capacity.
• If a request arrives when the bucket is empty, the request is rejected with HTTP 429 and a
Retry-After header indicating when the next token will be available.• The token bucket allows controlled bursts up to capacity, then enforces the steady-state rate sustainably.
For example: the POST /v1/messages endpoint has a bucket capacity of 200 tokens and a refill rate of 200 per second. You can fire 200 requests in the first 100 milliseconds (consuming the full bucket); after that, you sustain 200 per second indefinitely (consuming tokens as they arrive). If you stop sending for 1 second, the bucket refills to capacity and you have another full burst available.
The token bucket model is what most major REST APIs use (Stripe, GitHub, AWS, Atlassian) because it tolerates the natural burstiness of real applications without penalising clients whose traffic is uneven by design. A retry storm hitting at the same instant as a legitimate spike is a different story — that is what jitter exists to prevent, covered below.
Per-endpoint limits
Limits are scoped per API key, not per account. Generating multiple scoped keys for distinct callers (a separate key for the marketing platform, the suppression processor, the analytics dashboard) gives each caller its own bucket — useful when one caller's behaviour should not affect another's headroom.
| Endpoint | Burst capacity | Steady rate |
|---|---|---|
POST/v1/messages | 200 requests | 200/sec |
GET/v1/messages & /v1/messages/{id} | 100 requests | 50/sec |
POST/v1/suppressions (bulk: 1000 addrs/req) | 30 requests | 10/sec |
GET/v1/suppressions | 60 requests | 30/sec |
GET/v1/suppressions/{email} | 200 requests | 100/sec |
DELETE/v1/suppressions/{email} | 30 requests | 10/sec |
GET/v1/events (webhook event backfill) | 30 requests | 10/sec |
GET/v1/stats/* (reporting) | 20 requests | 5/sec |
POST/v1/webhooks (configuration changes) | 10 requests | 1/sec |
| All other endpoints (default) | 100 requests | 30/sec |
The shape of these limits reflects the cost of each endpoint. Send messages can burst high because they queue cheaply; reporting endpoints aggregate over large datasets and are intentionally conservative. The suppression bulk-add allows 1,000 addresses per request, so the effective rate is 10,000 addresses per second — enough for a 10 million address ESP migration import to complete in under 20 minutes.
An account-level secondary limit caps the aggregate across all keys at 1,000 requests per second to any single endpoint. This kicks in only when key-level fan-out becomes problematic; most senders never hit it.
Rate-limit headers
Every API response — not just 429 responses — includes the rate-limit headers. Read them on every response and you can proactively slow down before hitting the limit, instead of reactively backing off after the fact.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Burst capacity of this bucket. Useful for displaying capacity utilisation in monitoring dashboards. |
| X-RateLimit-Remaining | Tokens currently available. When this drops near 0, slow down; when it hits 0, the next request returns 429. |
| X-RateLimit-Reset | Unix epoch when the bucket will be at full capacity (assuming no further requests). Approximate — the bucket refills continuously. |
| X-RateLimit-Bucket | Identifier of the bucket consumed by this request. Useful when one application makes calls to multiple endpoints and you want to track each bucket independently. |
| Retry-After | Only present on 429 responses. Number of seconds to wait before the next attempt. Always non-zero. |
X-RateLimit-Remaining drops below 10% of X-RateLimit-Limit, your client should start adding small inter-request delays. This avoids the 429 entirely. A simple rule: if remaining < 20, sleep for (Reset - now) / Remaining seconds before the next request. The math works out to "spread the remaining requests evenly across the rest of the window."Handling 429 responses
When a request hits the rate limit, the response is HTTP 429 with Retry-After indicating the seconds to wait. The minimum correct handler is straightforward: if the response is 429, sleep for the Retry-After duration, then retry. Failing to handle 429 at all means the request is silently dropped — which for transactional sends is far worse than a delayed delivery.
Two design rules turn 429 handling from "barely works" into "production-grade":
- Always respect Retry-After. The header tells you exactly how long until the next token is available. Retrying earlier is wasteful and may extend the throttle if the server detects rapid retry loops (some APIs — OpenAI, GitHub — explicitly extend backoff for clients that retry too aggressively).
- Cap retries. A 429 that persists across 5+ retries usually indicates a structural problem (too many parallel callers, polling loop, runaway script) rather than a transient throttle. After the cap, surface the error to the caller. Never drop the request silently.
Exponential backoff with jitter
If Retry-After is somehow missing (malformed proxy, transient network issue), fall back to exponential backoff with jitter. The wait time doubles after each failure (1s, 2s, 4s, 8s...), and a small random offset is added to each delay. The randomisation matters: without jitter, a hundred clients hitting the limit at the same moment all retry at the same moment, producing a thundering herd that hits the limit again. With jitter, the retries spread out across a window, and the recovery is graceful.
Python
Node.js
Go
Distributed coordination
Rate limiting is straightforward when one process makes the requests. Distributed senders — multiple workers, multiple servers, autoscaled containers — need to coordinate so they collectively respect the per-key limit, not so that each instance independently believes it has the full quota. The pattern that scales:
- Centralised token tracker. Use Redis (or any shared cache) to track tokens consumed per bucket. Each worker decrements the shared counter atomically before making a request; if the decrement fails, the worker waits for refill rather than firing the request.
- Client-side limiter. A library that mirrors our bucket capacity locally and refills at the same rate. The library queues requests when the local bucket is empty, regardless of what the server says. This bounds your outgoing rate at the source.
- Backoff coordination on 429. When one worker hits 429, broadcast the Retry-After to all other workers (Redis pub-sub, or a shared "throttle until" key). All workers pause until the throttle clears, instead of independently retrying.
For most senders, option 2 (a client-side limiter) is enough. Open-source libraries exist for the major languages: aiolimiter (Python), bottleneck (Node.js), golang.org/x/time/rate (Go). They handle the bucket math, queueing, and steady-state pacing without you implementing it from scratch. Configure the limiter to slightly below our limit (90% is a safe default) and you will rarely see a 429.
Proactive client design
The patterns below shift work from runtime (handling 429s) to design time (avoiding them in the first place). Each is worth more than it costs to implement.
- Subscribe to webhooks instead of polling. The single largest source of unnecessary API traffic in our customer base is applications that poll
GET /v1/messages/{id}every few seconds to track delivery status. Webhooks deliver the same information within seconds of the actual event, with zero rate-limit budget consumed. - Batch suppression operations. The bulk endpoint accepts up to 1,000 addresses per request. Importing 100,000 addresses one-at-a-time consumes 100,000 tokens; the same operation as 100 batch requests consumes 100 tokens. Two orders of magnitude difference.
- Cache responses where appropriate. The list of suppressed addresses changes slowly. If you query it every minute to populate a cache, you save thousands of per-send check-this-address requests.
If-Modified-Sinceis supported on read endpoints and returns 304 (not counted against rate limit) when nothing has changed. - Use scoped keys to isolate limits. A runaway analytics dashboard hammering
/v1/stats/on the same key as your transactional sends will eventually trigger 429 on send requests. Separating into distinct keys means the dashboard's bad day does not impact transactional delivery. - Schedule large operations in the off-peak window. Bulk imports, full-list exports, and reporting jobs that touch large datasets benefit from running outside your peak send hours. The rate limits are the same, but the bucket is more likely to be at capacity, and a temporary throttle does not affect a real-time customer flow.
- Add jitter to scheduled jobs. A cron job that fires "every minute on the minute" produces a thundering herd of itself. Add 0–30 seconds of random offset; the work spreads across a window and stops competing with itself.
SMTP relay limits
SMTP relay sending is governed differently from REST API rate limits. The SMTP path uses per-IP, per-vMTA throttling at the PowerMTA layer, calibrated to the destination ISP rather than to a generic bucket. The reasoning: ISPs themselves enforce different rates for different domains, and a one-size-fits-all SMTP rate limit produces both wasted capacity (for ISPs that accept faster) and damaged reputation (for ISPs that throttle harder). The PowerMTA configuration adapts to each.
The headline numbers for SMTP relay capacity are determined by your plan tier and IP pool size, not by an API rate limiter. Specifically:
- Concurrent connections per IP — configured per ISP based on accepted concurrency for that destination (Gmail tolerates 10-20 concurrent inbound connections per source IP; Microsoft is closer to 5-10).
- Messages per connection — configured per ISP. Most major mailbox providers tolerate 50-100 messages per connection before requiring a new connection.
- Per-domain throttling — calibrated by the PowerMTA domain block configuration. This is documented in the operational notes on PowerMTA tuning rather than in this REST API reference.
Common pitfalls
Patterns that recur in incident reports often enough to flag explicitly. Each is operationally damaging; each is preventable in a few lines of code.
- Retrying on every 4xx, not just 429. A 401 is a credential problem. A 403 is a scope problem. A 422 is a validation problem. Retrying any of those produces the same failure forever, consuming rate-limit budget that legitimate requests need. The retry condition should be exactly: status == 429 OR (status >= 500 AND status <= 599).
- Sleeping on Retry-After: 0. The header is always non-zero on our responses, but proxies and CDNs occasionally rewrite values. Treat zero as "wait at least 1 second" defensively.
- Implementing exponential backoff without a cap. A 429 that persists for an hour produces a one-hour wait between retries by attempt 11 (if doubling from 1s without a cap). The legitimate cap is around 30 seconds for most production callers; longer than that and the retry is competing with whatever upstream system needed the request in the first place.
- No jitter on retries. The thundering herd is the failure mode this entire section exists to prevent. If your retry code does not include
random.uniform()or equivalent, it will produce a thundering herd the first time many clients hit 429 simultaneously. - Ignoring rate-limit headers on 200 responses. The headers are always present. Reading them lets you slow down before you hit the wall, instead of recovering after. The cost is one if-statement; the benefit is avoiding 429 entirely.
- Polling tight loops on transient errors. An endpoint returns 503 (server overload). Your client retries every 100ms for the next 60 seconds. By the time the endpoint is healthy, your client has consumed thousands of tokens for nothing. Apply backoff to 5xx responses too, not just 429.
- Sharing one API key across N parallel workers without coordination. Each worker thinks it has the full quota; collectively they exhaust it instantly. Either coordinate with a shared limiter or shard work onto separate keys.