Rate limits
Token buckets. There are three, they are independent, and any of them can reject a request.
Buckets are keyed by account and channel — not by API key. Issuing a second key does not buy you a second budget; both keys draw from the same customer bucket.
The three buckets
| Bucket | Keyed by | Burst | Refill | Applies to |
|---|---|---|---|---|
| Per-IP (pre-auth) | Client IP | 30 | 30/s | Every request, checked before your credential is even read |
| Per-customer | Your account | 60 | 1/s | Every authenticated route |
| Per-channel | Account + channel | 60 | 1/s | Any route whose path contains a channel id |
The per-IP gate exists so a flood of bad tokens from one address cannot burn our auth path. You will only meet it if you are hammering from a single IP.
What one request costs
One token on each bucket that applies. A call to
/v1/channels/{id}/messages spends one customer token and one channel token.
A call to /v1/channels spends only a customer token.
The bucket with the fewest tokens left decides the response, and
X-RateLimit-Scope tells you which one that was.
So sustained throughput is 1 request/second per channel, with a 60-request burst to absorb spikes. Size your fan-out accordingly: 500 messages on one channel takes roughly 440 seconds, not 500 milliseconds. Spread across channels or pace your sends.
Response headers
Authenticated responses carry:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Capacity of the deciding bucket. |
X-RateLimit-Remaining | Tokens left in it after this request was counted. |
X-RateLimit-Scope | Which bucket decided: customer, channel or ip. |
On 429 Too Many Requests you also get:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait. Honour it. |
There is no X-RateLimit-Reset header. Earlier revisions of this page
documented one; code reading it gets null. Use Retry-After on a 429, or
reset_at from GET /v1/limits.
The body is the standard envelope with code: "rate_limited":
{
"error": {
"code": "rate_limited",
"message": "Channel is over the per-second limit (60 burst). Retry in ~4s."
}
}Reading your headroom
Pass ?channel_id=<uuid> to fill in per_channel, or authenticate with a
channel-bound key and it is filled in automatically:
GET /v1/limits?channel_id=8d653c66-e4ff-43ee-97da-3de5ad5680d4
{
"per_customer": { "limit": 60, "remaining": 47, "reset_at": "2026-09-10T14:22:31.000Z" },
"per_channel": { "limit": 60, "remaining": 12, "reset_at": "2026-09-10T14:22:29.000Z" },
"channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
"per_key": null
}A per_channel of null means no channel was identified, not that no
channel limit applies. Since a channel send is charged to both buckets, real
headroom is min(per_customer.remaining, per_channel.remaining).
per_key is always null — no per-key bucket exists.
This call does spend a token, like every authenticated call — it just does not double-charge for the read itself.
Backoff pattern
async function callApi(url, init) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(url, init);
if (res.status !== 429) return res;
// Retry-After is authoritative — it is computed from the actual refill rate.
const wait = Number(res.headers.get('Retry-After') ?? 1) * 1000;
await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
}
throw new Error('Rate limited after 5 retries');
}Don't use exponential backoff against a 429 — Retry-After already reflects the
refill rate exactly. Reserve exponential backoff for transient 5xx.
What doesn't count
- Webhook deliveries originate from us and never touch your inbound budget.
- Public routes —
/healthz,/readyz,/v1/status/summary,/openapi.jsonand the signed media proxy/v1/media/{token}— are not charged to your account buckets. The per-IP gate still applies.
Related limits
These are separate caps, not token buckets:
| Limit | Value |
|---|---|
Concurrent /v1/events SSE streams | 10 per account (429 sse_connection_limit) |
| Media upload / send size | 25 MB |
| Message text length | 4096 characters |
| Caption length | 1024 characters |
client_ref length | 128 characters |
| Message history page size | 100 |
Need more?
Open a ticket (POST /v1/support/tickets) or email hello@whatisup.dev. Higher
limits are part of the Pro plan and bespoke for Scale.