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:

  1. The webhook field in the send request body.
  2. The webhookUrl on the Sender ID used.
  3. 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 channel field is an integer in messageStatus but a string (e.g. "SMS") in message. Branch on event before 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 2xx in well under 30 seconds; queue heavy work behind the acknowledgement.
  • At-least-once. Retries can duplicate events — deduplicate on messageId (or broadcastId for campaigns).
  • No ordering guarantee. Use the status value and timestamp, not arrival order.
  • Missed an event? Reconcile per-message state with GET /edrs?messageIds=....

See the Receive webhooks guide for a working receiver.