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:
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:
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.
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.
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
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:
- Record the attempt first. Before calling the API, write a row keyed by your own operation ID, such as
order-8842-confirm, with statuspending. - Send, then store the ID. On
200, save the returned message ID (orbulkSessionId/batchId) and mark the rowaccepted. A stored ID means the send is done: never send it again. - 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/listfor bulk jobs, or the batch list for other channels. - Make retries go through the same record. A queue redelivery or a restarted worker finds the existing row instead of sending again.
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
delayfield 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.
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: confirm delivery instead of resending.
- SMS API: bulk endpoints and duplicate filtering.
- SMPP: throughput, windowing and
command_statuscodes. - Authentication: fixing
401errors.
Ready to send your first message?
Request test credentials. An engineer sets up your account, and test sends go to your own verified numbers.
Request test access