---
title: "Webhooks and delivery statuses"
description: "Receive delivery statuses and inbound messages on your HTTPS endpoint, respond fast, deduplicate events, and poll or re-trigger statuses when needed."
canonical: "https://omnichannels.ai/developers/webhooks/"
language: "en"
updated: "2026-10-10"
publisher: "BroadNet Technologies"
brand: "OmniChannels.ai"
section: "Events and errors"
---

# Webhooks and delivery statuses

Canonical: https://omnichannels.ai/developers/webhooks/
Language: en
Publisher: OmniChannels.ai by BroadNet Technologies

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

1. Expose an HTTPS endpoint that accepts `POST` requests with a JSON body, for example `https://api.example.com/webhooks/omnichannels`.
2. 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.
3. 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.**

```json title="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.

```js title="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 title="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 `2xx` only after the event is stored.** If storage fails, return `5xx`.
- **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`. |

```bash title="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:

```bash title="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

1. Is the URL configured on the right account or sub-account?
2. Is the endpoint reachable from the internet over HTTPS, with a valid certificate?
3. Does it answer `2xx` quickly, or does it time out?
4. Is a firewall or WAF rule blocking the requests?
5. Are you logging the raw body before parsing, so a parsing error does not hide the event?

## Next steps

- [Errors and limits](/developers/errors-and-limits/): retries and duplicate protection on the sending side.
- [SMS API](/developers/sms/): bulk job control.
- [WhatsApp API](/developers/whatsapp/): batch insights in detail.
- [SMPP](/developers/smpp/): delivery receipts over an SMPP bind.
