Skip to content
Sendozi

Reliability

Rate limits and pagination

Send endpoints allow 300 requests per minute per workspace and return 429 rate_limit_exceeded with a retry delay. List endpoints use keyset cursor pagination with a default limit of 50 and a maximum of 100.

By SendoziUpdated 2 min read

The limit

SurfaceLimitScope
POST /v1/*/send and /v1/*/bulk300 requests per minutePer workspace

The limit counts requests, not recipients. A bulk request carrying a thousand recipients is one request, which is the main reason to prefer /v1/sms/bulk over a loop of single sends for a campaign.

Backing off correctly

On 429, read the delay named in the error's resolution and wait at least that long. Then retry with exponential backoff and jitter, and cap the number of attempts. Pair the retry with the same Idempotency-Key so a duplicate send is impossible.

A send loop that respects the limit
TypeScript
async function sendBatch(batches: SendPayload[], keyFor: (batch: SendPayload) => string) {
  for (const batch of batches) {
    let delay = 500;

    for (let attempt = 1; attempt <= 5; attempt += 1) {
      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": keyFor(batch),
        },
        body: JSON.stringify(batch),
      });

      const body = await response.json();
      if (body.success) break;

      if (response.status !== 429 && response.status < 500) {
        throw new Error(`${body.error.code}: ${body.error.message}`);
      }

      // Jitter stops a fleet of workers retrying in lockstep.
      await new Promise((resolve) => setTimeout(resolve, delay + Math.random() * 250));
      delay *= 2;
    }
  }
}
  • Group recipients into bulk requests rather than sending one request per recipient.
  • Spread a large campaign over time instead of firing everything in one minute.
  • Add jitter, or every worker in your fleet retries at the same instant.
  • Do not retry permanent errors - they consume the limit for nothing.

Pagination

GET /v1/messages and GET /v1/message-batches are keyset-paginated on created_at. Pass next_cursor back as cursor and stop when has_more is false. limit defaults to 50 and is capped at 100.

Paging through messages
cURL
curl "https://api.sendozi.com/v1/messages?limit=100" \
  -H "Authorization: Bearer $SENDOZI_API_KEY"
Response
{
  "success": true,
  "data": {
    "data": [ ],
    "has_more": true,
    "next_cursor": "2026-08-21T09:14:22.117Z"
  },
  "request_id": "req_8ecdcb4ce2ae4290"
}
Node.js
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);
}

Frequently asked questions

What is the Sendozi API rate limit?
300 requests per minute per workspace on the send and bulk endpoints. The limit counts requests, not recipients.
What should I do when I get 429 rate_limit_exceeded?
Wait at least the delay named in the error's resolution, then retry with exponential backoff and jitter, reusing the same Idempotency-Key so the retry cannot duplicate a send.
Does a bulk send of 1,000 recipients use 1,000 of my rate limit?
No. It is one request. That is why bulk is the right endpoint for a campaign.
How many results can one page return?
100 at most; the default is 50. Follow next_cursor until has_more is false.

Pair backoff with idempotency

A retry is only safe when the same key guarantees it cannot send twice.