Reliability
Limits, large sends and pagination
Send endpoints allow 300 requests a minute per workspace, and one request can carry up to 1,000,000 recipients. A send is accepted into a queue and handed to the networks at a steady rate, so a large campaign takes time to go out. Lists use cursor pagination.
The limits
| What | Limit |
|---|---|
| POST /v1/sms/send and /v1/sms/bulk | 300 requests a minute per workspace, counted together |
| Recipients in one send request | 1,000,000 |
| Request body | 32 MB |
| Webhook endpoint create, update and delete | 20 a hour per workspace |
| Transactional sends per Sender ID | The per-minute and per-day limits on its profile |
| Idempotency-Key | 255 characters |
The send limit counts requests, not recipients: a request with 50,000 recipients is one request. Send a campaign as one request (or a few), not one request per number.
Over a limit, the request returns 429 rate_limit_exceeded with a Retry-After header and retry_after_seconds in the error. Wait that long, then retry with the same Idempotency-Key.
Large sends
- 1
Accepted
Every number is validated, duplicates and opted-out numbers are removed, the full cost is reserved from the wallet, and each recipient is queued. Then the request returns 202 with the batch_id. Nothing has been sent yet.
- 2
Sent
Sendozi's workers hand queued recipients to the networks at a steady, platform-wide rate. Transactional messages are always sent first. Several batches sending at once share the rate, so a small send is not stuck behind a large one. If the network asks Sendozi to slow down, or stops answering, sending pauses for 15 seconds to 2 minutes and the waiting recipients go out when it resumes, rather than failing.
- 3
Reported
Delivery reports arrive as the networks return them, as message.delivered and message.failed webhooks. batch.completed arrives once every recipient has been sent, failed or skipped.
- A large campaign is sent over minutes or hours, not instantly. Track it with
GET /v1/message-batches/{batch_id}, whoseprogresscounts move as it goes. - Live promotional SMS goes out 08:30 to 19:30 WAT. A promotional batch still queued when the window closes pauses and continues when it next opens. Schedule big promotional sends early in the day.
- The request that queues hundreds of thousands of recipients takes longer to answer, because every recipient is checked and queued before the 202. Set a client timeout of several minutes for such a request, and always send an Idempotency-Key so a timed-out request can be retried without queuing it twice.
- A campaign can also be split across several requests, each with its own Idempotency-Key. Each becomes its own batch.
- You can stop a batch at any time with
POST /v1/message-batches/{batch_id}/cancel. The unsent part of the reserved cost is returned to the wallet.
import { readFile } from "node:fs/promises";
const numbers = (await readFile("audience.txt", "utf8")).split(/\r?\n/).filter(Boolean);
const response = await fetch("https://api.sendozi.com/v1/sms/bulk", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDOZI_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "campaign-2026-09-24-october-offer",
},
body: JSON.stringify({
sender: "YourBrand",
route: "promotional",
recipients: numbers,
message: "October offer: 20% off everything until Sunday. Opt out: acme.ng/stop",
}),
signal: AbortSignal.timeout(5 * 60_000), // queuing 200k recipients takes a while
});
const body = await response.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
console.log(body.data.batch_id, body.data.accepted_count, body.data.reserved_cost_kobo);Backing off
- Honour
Retry-Afteron 429. Without it, back off exponentially with jitter, so a fleet of workers does not retry in step. - Reuse the same Idempotency-Key and the same request body on every retry.
- Cap the attempts, and do not retry 4xx errors other than 429: they will repeat until the request is fixed.
Pagination
List endpoints return { data, has_more, next_cursor } inside the envelope. Pass next_cursor back as cursor until has_more is false. For messages and batches, limit defaults to 50 and is capped at 100; for a batch's recipients, 100 and 500.
curl "https://api.sendozi.com/v1/messages?limit=100" \
-H "Authorization: Bearer $SENDOZI_API_KEY"
# then, with the next_cursor from that page:
curl "https://api.sendozi.com/v1/messages?limit=100&cursor=2026-09-24T09:14:22.117Z" \
-H "Authorization: Bearer $SENDOZI_API_KEY"export async function* allMessages() {
let cursor;
do {
const url = new URL("https://api.sendozi.com/v1/messages");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SENDOZI_API_KEY}` },
});
const body = await response.json();
if (!body.success) throw new Error(body.error.code);
yield* body.data.data;
cursor = body.data.has_more ? body.data.next_cursor : undefined;
} while (cursor);
}- Results are newest first for messages and batches, and in queue order for a batch's recipients.
- Use
next_cursorexactly as returned. Pages stay stable while new messages arrive. - Sender IDs, webhook endpoints and API keys are returned as one list, without pagination.
Frequently asked questions
- What is the Sendozi API rate limit?
- 300 send requests a minute per workspace, across /v1/sms/send and /v1/sms/bulk together. It counts requests, not recipients.
- How many recipients can one Sendozi API request carry?
- Up to 1,000,000, within a 32 MB request body. Each request becomes one batch.
- How long does a send to 1,000,000 numbers take to go out?
- It is accepted in one request and then sent at Sendozi's steady platform rate, shared with other traffic, so it goes out over hours rather than seconds. Live promotional sends go out only between 08:30 and 19:30 WAT. Follow progress on the batch.
- What should I do when I get 429 rate_limit_exceeded?
- Wait for the number of seconds in the Retry-After header, then retry with the same Idempotency-Key and body.
Related reading
Sending
Sending SMS
POST /v1/sms/send and /v1/sms/bulk: every request field, the response, how recipients are cleaned, limits, the delivery window, and exactly how a send is priced and charged.
Sending
Messages, batches and delivery status
Every send creates a batch of recipients, each becoming a message. How to look them up, what each status means, why a message failed, how to page through lists, and how to cancel a send.
Reliability
Idempotency and safe retries
How the Idempotency-Key header works on Sendozi send endpoints, what a repeat returns, why the body must be byte-for-byte identical, how long keys last, and how to choose one.
Reliability
Errors and the response envelope
The Sendozi response envelope, every error code the API returns with its HTTP status and fix, which errors are safe to retry, Retry-After, and how request_id traces a request.