---
title: "Errors, retries and rate limits"
description: "HTTP status codes and error bodies, which errors to retry with backoff and jitter, default rate limits, pagination, and how to avoid duplicate sends."
canonical: "https://omnichannels.ai/developers/errors-and-limits/"
language: "en"
updated: "2026-10-10"
publisher: "BroadNet Technologies"
brand: "OmniChannels.ai"
section: "Events and errors"
---

# Errors, retries and rate limits

Canonical: https://omnichannels.ai/developers/errors-and-limits/
Language: en
Publisher: OmniChannels.ai by BroadNet Technologies

The API reports errors with standard HTTP status codes and a JSON body. This page explains each code, which ones to retry, the default rate limits, pagination, and how to keep retries from sending a message twice.

## HTTP status codes

| Code | Meaning | What to do |
| --- | --- | --- |
| `200` | Success. | Store the returned ID. |
| `201` | Created (for example an onboarding link). | Use the returned resource. |
| `400` | Validation error or missing required field. | Fix the request. Do not retry unchanged. |
| `401` | Missing or invalid Bearer token. | Fix the token. Do not retry. |
| `403` | Token is valid but not allowed to call this operation. | Ask us to enable the operation. Do not retry. |
| `404` | Resource not found, such as a template, sender or batch. | Check the name or ID. |
| `413` | Uploaded file exceeds the size limit. | Split the file or reduce media size. |
| `429` | Rate limit exceeded. | Wait for `Retry-After`, then retry. |
| `500` | Unexpected server error. | Retry with exponential backoff. |
| `502` | An upstream provider rejected the call. For WhatsApp, `message` carries Meta's error code and text. | Read the message. Retry only if the upstream error is transient. |
| `503`, `504` | Temporarily unavailable or timed out upstream. | Retry with exponential backoff. |

## Error bodies

Errors raised by the API use a five-field body:

```json title="400 Bad Request"
{
  "status": 400,
  "error": "Bad Request",
  "message": "<what went wrong>",
  "errorCode": "<machine-readable code>",
  "requestUri": "/sms/sendSms"
}
```

| Field | Description |
| --- | --- |
| `status` | The HTTP status code. |
| `error` | The HTTP reason phrase. |
| `message` | A human-readable explanation. Log it; do not parse it. |
| `errorCode` | A machine-readable code. Branch on this, after the HTTP status. |
| `requestUri` | The path that failed. |

The `message` and `errorCode` values above are placeholders; log the real values you receive. A missing or invalid token is rejected earlier, at the API gateway, with a two-field body:

```json title="401 Unauthorized"
{
  "error": "Unauthorized",
  "message": "Missing Authorization token"
}
```

Handle errors in this order: HTTP status first, then `errorCode`. Log `message` and `requestUri` with your own request ID. SMPP binds return standard SMPP `command_status` codes instead; see [SMPP](/developers/smpp/).

## Retry with backoff and jitter

Retry only what can succeed later: network errors, `429`, and `5xx`. Never retry `4xx` validation or authentication errors unchanged.

| Outcome | Retry? |
| --- | --- |
| `200` with an ID | No. The message was accepted. |
| `400`, `401`, `403`, `404`, `413` | No. Fix the cause first. |
| `429` | Yes, after `Retry-After`. |
| `500`, `502`, `503`, `504` | Yes, with exponential backoff and jitter. |
| Timeout or connection reset | Only after checking that the first attempt was not accepted. |

Wait longer after each attempt, add random jitter so many clients do not retry in step, and cap both the delay and the number of attempts.

```js title="Node.js"
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function sendWithRetry(body, { maxAttempts = 5, baseMs = 500, capMs = 30000 } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch("https://api.broadnet.me/sms/sendSms", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.BROADNET_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(15000),
    });
    // A thrown timeout or network error propagates: check status before resending.

    if (res.ok) return res.text(); // message ID
    if (!RETRYABLE.has(res.status) || attempt === maxAttempts) {
      throw new Error(`Send failed: ${res.status} ${await res.text()}`);
    }

    const retryAfter = Number(res.headers.get("Retry-After"));
    const backoff = Math.min(capMs, baseMs * 2 ** (attempt - 1));
    const delay = retryAfter > 0 ? retryAfter * 1000 : Math.random() * backoff; // full jitter
    await new Promise((r) => setTimeout(r, delay));
  }
}
```

```python title="Python"
import os
import random
import time

import requests

RETRYABLE = {429, 500, 502, 503, 504}


def send_with_retry(body, max_attempts=5, base=0.5, cap=30.0):
    for attempt in range(1, max_attempts + 1):
        r = requests.post(
            "https://api.broadnet.me/sms/sendSms",
            headers={"Authorization": f"Bearer {os.environ['BROADNET_TOKEN']}"},
            json=body,
            timeout=15,
        )  # a timeout raises: check status before resending
        if r.ok:
            return r.text  # message ID
        if r.status_code not in RETRYABLE or attempt == max_attempts:
            raise RuntimeError(f"Send failed: {r.status_code} {r.text}")

        retry_after = r.headers.get("Retry-After")
        if retry_after and retry_after.isdigit():
            delay = int(retry_after)
        else:
            delay = random.uniform(0, min(cap, base * 2 ** (attempt - 1)))  # full jitter
        time.sleep(delay)
```

## Avoid duplicate sends

The API does not accept an idempotency key. A request can succeed on the server while its response is lost on the way back, and a blind retry then sends the message twice. Build duplicate protection into your integration:

1. **Record the attempt first.** Before calling the API, write a row keyed by your own operation ID, such as `order-8842-confirm`, with status `pending`.
2. **Send, then store the ID.** On `200`, save the returned message ID (or `bulkSessionId` / `batchId`) and mark the row `accepted`. A stored ID means the send is done: never send it again.
3. **After a timeout, check before resending.** Look for evidence that the first attempt was accepted: a webhook status for that number and text, a new entry in `GET /sms/list` for bulk jobs, or the batch list for other channels.
4. **Make retries go through the same record.** A queue redelivery or a restarted worker finds the existing row instead of sending again.

```js title="Record before send (Node.js)"
const opId = "order-8842-confirm";
const inserted = await db.sends.insertIfAbsent({ opId, status: "pending" });
if (!inserted) return; // another worker already owns this send

const messageId = await sendWithRetry({
  senderId: "OmniDemo",
  destinationNumber: "447700900123",
  message: "Your order 8842 is confirmed.",
  messageType: "TEXT",
});
await db.sends.update({ opId }, { status: "accepted", messageId });
```

For bulk sends, set `allowDuplicate` to `false` so duplicate numbers in one upload are removed.

## Rate limits

These are the documented default limits. Your account's limits are confirmed with your credentials, and higher ceilings can be agreed for production traffic.

| Interface | Default limit | Notes |
| --- | --- | --- |
| REST API | 200 requests per second, sustained | Bursts up to 500 requests per second for up to 60 seconds. |
| SMPP | 1,000 messages per second per bind | Window of 100 outstanding `submit_sm`. More throughput through more binds, by agreement. |
| Number lookup (HLR) | 50 requests per second | Applies to `POST /sms/lookup`. |

When you exceed a limit, the API answers `429` with a `Retry-After` header. Wait at least that long before retrying, and slow your send rate.

Practical tips:

- Spread large sends with the bulk endpoints (one request for many recipients) rather than thousands of single requests.
- Use the `delay` field on bulk and channel sends to throttle delivery.
- Run your own token bucket in front of the API so bursts from several workers stay within your limit.
- Plan peaks (flash sales, alerts) with our team in advance.

## Pagination

List endpoints that can return many records take two query parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `page` | integer | Zero-based page number. Start at `0`. |
| `size` | integer | Records per page, for example `50`. |

Request page `0`, then increase `page` until a page returns fewer records than `size`.

```bash title="cURL"
curl "https://api.broadnet.me/whatsapp/template/list?page=0&size=50" \
  -H "Authorization: Bearer $BROADNET_TOKEN"
```

Some batch lists take `page` and `size` in the request body instead (for example `POST /rcs/message/batches` and `POST /whatsapp/message/all-bulk`), and many lists also accept date filters, which are faster than paging through everything. `GET /sms/list` is not paginated.

## Next steps

- [Webhooks](/developers/webhooks/): confirm delivery instead of resending.
- [SMS API](/developers/sms/): bulk endpoints and duplicate filtering.
- [SMPP](/developers/smpp/): throughput, windowing and `command_status` codes.
- [Authentication](/developers/authentication/): fixing `401` errors.
