The platform pushes delivery statuses, and inbound messages, to the webhook URL configured on your account, so you can track every message without polling. This page covers the status lifecycle, a production-ready handler, and what to do when events go missing.
Set up your endpoint#
- Expose an HTTPS endpoint that accepts
POSTrequests with a JSON body, for examplehttps://api.example.com/webhooks/omnichannels. - Send the URL to our team (or your account admin) to configure it on your account. Resellers can configure a separate webhook per sub-account.
- Send a test message to your own number and confirm that the events arrive.
During development, a tunnel such as ngrok gives your local server a public HTTPS URL.
Delivery statuses#
Every channel reports the same normalised statuses:
| Status | Meaning | Final? |
|---|---|---|
SUBMITTED |
Accepted by the platform and queued for the network or channel. | No |
SENT |
Handed to the operator or channel. | No |
DELIVERED |
Confirmed on the recipient's device. | Usually; READ may follow |
READ |
Opened by the recipient, on channels that report reads (such as WhatsApp). | Yes |
FAILED |
Not delivered. The event carries a reason where the channel provides one. | Yes |
REJECTED |
Refused by the operator or channel, for example an unregistered sender ID or blocked content. | Yes |
Treat statuses as a progression that can arrive out of order. A late SENT should never overwrite a DELIVERED you have already stored. Also expect new values over time, and do not fail on a status you do not recognise.
Payload#
Payload fields vary by channel, and the API spec does not define a single webhook schema. Log the raw body first, then map the fields you need. The example below shows the kind of information a delivery event carries. It is illustrative only; do not code against these field names without checking a real event from your account.
Illustrative delivery event (field names vary by channel)
{
"messageId": "2603121407537573731",
"channel": "SMS",
"destination": "447700900123",
"status": "DELIVERED",
"timestamp": "2026-10-10T09:15:42Z",
"reason": null
}Ask your onboarding engineer for sample payloads for each channel you use, and keep them as fixtures in your tests.
Write the handler#
Do three things in order: store the raw event, acknowledge with a 2xx, then process it asynchronously.
Node.js (Express)
import express from "express";
const app = express();
// Capture the raw body so you can store it exactly as received.
app.post(
"/webhooks/omnichannels",
express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); } }),
async (req, res) => {
try {
await db.webhookEvents.insert({ receivedAt: new Date(), body: req.rawBody });
} catch (err) {
return res.sendStatus(500); // storage failed: let the event be retried
}
res.sendStatus(200); // acknowledge fast
queue.publish("status-events", req.body); // do the real work elsewhere
},
);
// Worker: idempotent processing
async function processEvent(event) {
const id = event.messageId ?? event.message_id ?? event.id; // map per channel
const status = event.status;
const rank = { SUBMITTED: 1, SENT: 2, DELIVERED: 3, READ: 4, FAILED: 4, REJECTED: 4 };
const current = await db.messages.findOne({ providerId: id });
if (!current) return; // unknown message: log it and investigate
if ((rank[status] ?? 0) <= (rank[current.status] ?? 0)) return; // duplicate or stale
await db.messages.update({ providerId: id }, { status, updatedAt: new Date() });
}Python (Flask)
from datetime import datetime, timezone
from flask import Flask, request
app = Flask(__name__)
RANK = {"SUBMITTED": 1, "SENT": 2, "DELIVERED": 3, "READ": 4, "FAILED": 4, "REJECTED": 4}
@app.post("/webhooks/omnichannels")
def omnichannels_webhook():
raw = request.get_data(as_text=True)
try:
db.webhook_events.insert({"received_at": datetime.now(timezone.utc), "body": raw})
except Exception:
return "", 500 # storage failed: let the event be retried
queue.publish("status-events", raw) # process asynchronously
return "", 200 # acknowledge fast
def process_event(event: dict) -> None:
msg_id = event.get("messageId") or event.get("message_id") or event.get("id") # map per channel
status = event.get("status")
current = db.messages.find_one({"provider_id": msg_id})
if current is None:
return # unknown message: log it and investigate
if RANK.get(status, 0) <= RANK.get(current["status"], 0):
return # duplicate or stale event
db.messages.update({"provider_id": msg_id}, {"status": status})Rules for a reliable handler#
- Respond within a few seconds. Do not call other services, send messages or run heavy queries before you respond.
- Return
2xxonly after the event is stored. If storage fails, return5xx. - Deduplicate by message ID. The same status can arrive more than once. Make processing idempotent: applying an event twice must have the same effect as applying it once.
- Never resend a message from the webhook handler. Decide on retries in a separate job, after checking the status.
- Keep the raw body for troubleshooting and for replaying events after a bug fix.
Secure the endpoint#
The API spec does not define a webhook signature header, so do not wait for one. Instead:
- Use HTTPS only, with a valid certificate.
- Use a long, unguessable path, and rotate it if it leaks.
- Validate the body shape and reject anything unexpected.
- Treat events as notifications. Before an irreversible action (a refund, a fraud lock), confirm the state with a polling call.
When events are missing#
If your endpoint was down, or a status seems stuck, you have three options.
| Need | Call | Notes |
|---|---|---|
| Re-push SMS delivery receipts | POST /sms/resendDlr |
Filter by startDate, endDate, senderId and messageStatus. |
| Check SMS bulk job progress | GET /sms/list |
Processed and pending counts per job. |
| Read WhatsApp status per recipient | GET /whatsapp/message/bulk-insights |
Filter a batch by status or destination; paginated with page and size. |
Re-push DLRs
curl -X POST https://api.broadnet.me/sms/resendDlr \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startDate": "2026-10-09",
"endDate": "2026-10-10",
"senderId": "OmniDemo",
"messageStatus": "DELIVERED"
}'For WhatsApp, read the status of each recipient in a batch:
WhatsApp per-recipient status
curl "https://api.broadnet.me/whatsapp/message/bulk-insights?batch_Id=2609091405000000001&page=0&size=100" \
-H "Authorization: Bearer $BROADNET_TOKEN"The other channels have batch list endpoints too (for example POST /rcs/message/batches and POST /push/message/batches); see each channel's page.
Troubleshooting checklist#
- Is the URL configured on the right account or sub-account?
- Is the endpoint reachable from the internet over HTTPS, with a valid certificate?
- Does it answer
2xxquickly, or does it time out? - Is a firewall or WAF rule blocking the requests?
- Are you logging the raw body before parsing, so a parsing error does not hide the event?
Next steps#
- Errors and limits: retries and duplicate protection on the sending side.
- SMS API: bulk job control.
- WhatsApp API: batch insights in detail.
- SMPP: delivery receipts over an SMPP bind.
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