Receive webhooks
Goal: receive delivery statuses and inbound replies at your own HTTPS endpoint.
Prerequisites: a publicly reachable HTTPS URL that can accept POST requests.
Sending is asynchronous: POST /messages returns immediately, and the delivery lifecycle (sent → delivered, or failed) is reported to your endpoint afterward as a JSON POST. Campaigns report their status the same way.
Alaris documents a different flow: the vendor calls Alaris with GET/POST query parameters (command=deliver, dlvrMsgId, dlvrMsgStat, username, password). That is inbound to the platform, not this client webhook.
Steps
1. Decide where events go
The platform picks the first URL that is set, in this order:
- The
webhookfield in the send request body. - The
webhookUrlconfigured on the Sender ID. - The webhook configured on the API key.
If none is set, the event is dropped — your only recourse is polling GET /edrs.
2. Point a send at your endpoint
curl -X POST https://client.routeon.io/api/v1/messages
-H "X-API-Key: <API_KEY>" -H "Content-Type: application/json"
-d '{
"contact": "905063565285",
"channel": 1,
"senderId": "Routeon",
"payload": { "text": "Hello from Routeon" },
"webhook": "https://your.app/hooks/routeon"
}'
3. Handle the event types
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": "" }
}
message — inbound (mobile-originated) reply from the recipient:
{
"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" }
}
}
Note:
channelis an integer inmessageStatus(e.g.1) but a string inmessage(e.g."SMS"). Branch oneventfirst.
Campaign status — for broadcasts, one event per state change:
{ "status": "completed", "webhook": "https://your.app/hooks/routeon", "broadcastId": 12345, "message": null }
4. Minimal receiver
import express from "express";
const app = express();
app.use(express.json());
app.post("/hooks/routeon", (req, res) => {
const body = req.body;
// Acknowledge fast; do heavy work asynchronously.
res.status(200).end();
if (body.event === "messageStatus") {
// dedupe on body.message.messageId, then update your records
} else if (body.event === "message") {
// inbound reply — body.message.payload.text
} else if (body.broadcastId) {
// campaign status — body.status
}
});
app.listen(8080);
Verify
Send a test message with webhook pointed at your endpoint and confirm you receive a messageStatus event with a delivered (or failed) status carrying the same messageId you got back from the send.
Delivery semantics
- No signature header. Requests are plain HTTPS
POSTs. Protect the endpoint by using an unguessable URL path, serving it over HTTPS, and — if possible — restricting inbound traffic to the platform. - Respond
2xxquickly (well under 30 seconds). Queue heavy processing behind a fast acknowledgement. - Treat delivery as at-least-once. Retries can produce duplicates — deduplicate on
messageId(orbroadcastIdfor campaigns). - If an event is missed, reconcile with
GET /edrs?messageIds=....
Common failures
| Symptom | Cause | Fix |
|---|---|---|
| No events arrive | No webhook resolved | Set webhook in the request, on the Sender, or on the key |
| Duplicate events | At-least-once retries | Dedupe on messageId / broadcastId |
| Events stop under load | Slow receiver / timeouts | Acknowledge 200 immediately, process async |
channel parsing errors |
Type differs by event | Branch on event; channel is int in messageStatus, string in message |