Rate limits
Two separate ceilings: how fast you may call the API, and how fast each operator accepts traffic.
API limits
| Endpoint | Rate limit | Payload limit |
|---|---|---|
| Every endpoint | 60 requests / minute, per key | — |
| POST /sms/send | — | 1,000 recipients per request |
| POST /contacts/import | — | 50,000 contacts per request |
Every response carries your current window state:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 41
X-RateLimit-Reset: 1755417600
Retry-After: 18 # only on 429How the window works
The window is fixed, not rolling: the counter resets at the top of each minute, and X-RateLimit-Reset is the exact moment it will. That means a short burst inside one minute is fine, which is deliberate — the limit exists to stop a runaway loop, not to meter you like a utility.
Rotating a key resets its budget, because the budget belongs to the key. That is a useful escape hatch and a rare enough event to be harmless.
Operator throughput
Accepting a message is not the same as delivering it. A send returns 202 Accepted as soon as it is queued and paid for; the messages then drain into each network at the rate that operator has provisioned, so a large campaign takes minutes rather than seconds to actually go out.
The provisioned rate differs per network and per contract, so there is no single number worth publishing here. What matters for your code is that delivery is asynchronous: poll GET /v1/sms/{messageId} or, better, subscribe to delivery webhooks.
Handling 429
async function sendWithRetry(payload, attempt = 0) {
const res = await fetch(`${BASE}/sms/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SBS_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": payload.reference,
},
body: JSON.stringify(payload),
});
if (res.status !== 429) return res.json();
if (attempt >= 5) throw new Error("rate limited after 5 attempts");
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, wait * 1000));
return sendWithRetry(payload, attempt + 1);
}Need more headroom?

