Webhooks & Events
Sending is asynchronous. POST /messages and POST /omnimessages return immediately once the platform accepts the request; the real delivery lifecycle and any inbound replies are reported to your HTTPS endpoint as webhook events. Campaigns report their status the same way.
Event catalog
| Event | Fires when | Payload highlights |
|---|---|---|
messageStatus |
A message changes delivery state (sent, delivered, displayed, failed, undeliverable) |
message.{messageId, status, contact, cost}, reason.{code, text} |
message |
The recipient sends an inbound (mobile-originated) reply | message.{messageId, channel, contact, payload} |
| Campaign status | A broadcast changes state (CREATED, STARTED, FINISHED, FAILED, CANCELED) |
status, broadcastId, message (reason on failure) |
Where events are delivered
When the platform dispatches an event it uses the first webhook URL that is set, in this order:
- The
webhookfield in the send request body. - The
webhookUrlon the Sender ID used. - The webhook configured on the API key.
If none is set, the event is dropped — retrieve statuses with GET /edrs instead.
Payloads
messageStatus — delivery lifecycle
{
"event": "messageStatus",
"message": {
"messageId": "4aab1947-7a34-4efe-8c51-e9c8f9b1412b",
"omniTransactionId": "57f29fdd-1127-45b3-a7d5-260a831a4662",
"channel": 1,
"senderId": "Routeon",
"status": "delivered",
"timestamp": "2023-09-08T15:30:00Z",
"contact": "905324546496",
"cost": "1.00"
},
"reason": { "code": 0, "text": "" }
}
| Field | Type | Description |
|---|---|---|
event |
string | Always messageStatus. |
message.messageId |
string | The id returned from the send. |
message.omniTransactionId |
string | Set for messages sent via POST /omnimessages. |
message.channel |
integer | Channel enum (1 = SMS). |
message.status |
string | sent | delivered | displayed | failed | undeliverable | unknown. |
message.timestamp |
string | RFC 3339. |
message.contact |
string | Recipient. |
message.cost |
string | Billed amount; may be empty. |
reason.code / reason.text |
int / string | Optional failure code and human-readable reason. |
message — inbound reply (MO)
{
"event": "message",
"message": {
"messageId": "4aab1947-7a34-4efe-8c51-e9c8f9b1412b",
"omniTransactionId": "57f29fdd-1127-45b3-a7d5-260a831a4662",
"timestamp": "2023-09-08T15:30:00Z",
"channel": "SMS",
"senderId": "2813380676",
"contact": "8172958708",
"payload": { "text": "test SMS" }
}
}
The
channelfield is an integer inmessageStatusbut a string (e.g."SMS") inmessage. Branch oneventbefore reading it.
Campaign status
One event per state transition — not one per recipient.
{ "status": "FINISHED", "webhook": "https://your.app/hooks/routeon", "broadcastId": 12345, "message": null }
On failure the message field carries the reason:
{ "status": "FAILED", "webhook": "https://your.app/hooks/routeon", "broadcastId": 12346, "message": "All recipient phone numbers were rejected by moderation" }
Delivery semantics
- No signature header. Events are plain HTTPS
POSTs. Protect your endpoint with an unguessable URL path, HTTPS, and — where possible — source restrictions. - Acknowledge fast. Return
2xxin well under 30 seconds; queue heavy work behind the acknowledgement. - At-least-once. Retries can duplicate events — deduplicate on
messageId(orbroadcastIdfor campaigns). - No ordering guarantee. Use the
statusvalue andtimestamp, not arrival order. - Missed an event? Reconcile per-message state with
GET /edrs?messageIds=....
See the Receive webhooks guide for a working receiver.