Unified Messaging Webhooks
Webhooks provide real-time delivery notifications for messages sent through the Unified Messaging API. Instead of polling the delivery status endpoint, your server receives HTTP POST callbacks as each message progresses through the delivery lifecycle — from queued to delivered (or failed).
This page covers webhook setup, the notification payload structure, event types, and how to build a webhook handler for unified messaging.
Delivery webhooks fire for messages sent through the Unified Messaging endpoint (POST /messaging/v1/messages). For incoming messages (replies from recipients), notifications are sent to the webhook configured against the individual channel (SMS, WhatsApp, or RCS), not the unified messaging webhook.
Webhook Setup
Configure your webhook URL in the EnableX Portal under your project settings. Navigate to My Projects → [Your Project] → Unified Messaging → Webhook.
Requirements
| Requirement | Details |
|---|---|
| Protocol | HTTPS — all webhook posts use HTTPS. Your server must accept HTTPS requests. |
| TLS/SSL Certificate | A valid TLS/SSL certificate must be installed. Self-signed certificates are not supported. |
| Request Format | JSON content is posted as raw body (not form-encoded). Your handler must read the raw request body. |
| Response | Your server must respond with HTTP 200 in the same connection. Any other response code triggers a retry. |
Every webhook notification must be acknowledged by returning an HTTP 200 response. If your server does not respond with 200, EnableX will retry the notification. Process the delivery event asynchronously if needed, but always return 200 immediately.
Securing Your Webhook — HTTP Basic Authentication
EnableX supports HTTP Basic Authentication for Unified Messaging webhook delivery. When enabled, EnableX includes your credentials in every webhook POST so your server can verify the request is genuinely from EnableX before processing the payload.
To enable this, check the HTTP Authentication checkbox in the webhook configuration screen in the EnableX Portal and enter a username and password. EnableX will include these credentials in the Authorization header of every Unified Messaging notification sent to your endpoint.
Your webhook URL must be publicly reachable over HTTPS. During local development, use a tunnelling tool such as ngrok http 3000 to expose your local server, then paste the generated HTTPS URL into the portal.
Acknowledgement Examples
<?php
// Read the raw JSON body
$payload = json_decode(file_get_contents('php://input'), true);
// Immediately acknowledge receipt
header("HTTP/1.1 200 OK");
// Process the event asynchronously
processUnifiedEvent($payload);
?>
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/unified', (req, res) => {
// Immediately acknowledge receipt
res.sendStatus(200);
// Process the event asynchronously
processUnifiedEvent(req.body);
});
app.listen(3000);
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/unified', methods=['POST'])
def unified_webhook():
payload = request.get_json()
# Process the event asynchronously
process_unified_event(payload)
# Immediately acknowledge receipt
return '', 200
Delivery Notification Payload
Every webhook notification includes the overall message status and per-channel delivery details. The structure mirrors the delivery status API response, so you can use the same parsing logic for both.
Payload Structure
The notification payload has two levels:
- Top level — Message identifiers (
message_id,to), the channels list, and the preference mode. - Delivery object — Overall delivery status, the current/final channel, and sub-objects for each channel that was attempted with channel-specific timestamps and status.
Full Notification Payload
{
"message_id": "65f1a2b3c4d5e6f7a8b9c0d1",
"to": "+919876543210",
"channels": [
"whatsapp",
"sms",
"rcs"
],
"preference": "order",
"delivery": {
"status": "DELIVERED",
"channel": "whatsapp",
"sms": {
"message_id": "sms_ch_abc123",
"status": "FAILED",
"queued": "2026-04-09T10:00:05Z",
"sent": null,
"delivered": null,
"sender": "ENABLEX",
"failure_reason": "SENDER_NOT_APPROVED"
},
"whatsapp": {
"message_id": "wamid_def456",
"recipient_id": "+919876543210",
"status": "DELIVERED",
"queued": "2026-04-09T10:00:00Z",
"sent": "2026-04-09T10:00:01Z",
"delivered": "2026-04-09T10:00:03Z",
"business_phone": "+6531234567",
"conversation": {
"id": "conv_xyz789",
"type": "BIC"
}
},
"rcs": {
"event_type": "DELIVERED",
"message_id": "rcs_msg_ghi012",
"message_status": "DELIVERED",
"queued": "2026-04-09T10:00:00Z",
"sent": "2026-04-09T10:00:01Z",
"delivered": "2026-04-09T10:00:02Z",
"agent": "MY_RCS_AGENT",
"failure_reason": null
}
}
}
Field Reference
| Field | Type | Description |
|---|---|---|
message_id | String | Unique message identifier from the send response |
to | String | Recipient phone number (E.164) |
channels | Array | Channels in resolved order. If preference was budget or priority, this may differ from the original request. |
preference | String | The preference mode used: order, budget, or priority |
delivery.status | String | Overall delivery status: PENDING, QUEUED, SENT, DELIVERED, or FAILED |
delivery.channel | String | The channel the message was last attempted or delivered through |
Per-Channel Status Fields
Each channel sub-object (delivery.sms, delivery.whatsapp, delivery.rcs) appears only if the message was queued on that channel. Common fields across all channels:
| Field | Type | Description |
|---|---|---|
message_id | String | Channel-specific message identifier |
status | String | Channel delivery status: PENDING, QUEUED, SENT, DELIVERED, FAILED |
queued | String | Timestamp when the message was queued on this channel |
sent | String | Timestamp when the message was sent to the provider |
delivered | String | Timestamp when delivery was confirmed |
failure_reason | String | Reason for failure (if applicable) |
Channel-Specific Additional Fields
| Channel | Additional Fields | Description |
|---|---|---|
| SMS | sender | Sender ID or phone number used |
recipient_id, business_phone, conversation | Recipient number, business number, and conversation object (with id and type: UIC or BIC) | |
| RCS | event_type, message_status, agent | RCS-specific event type, message status, and agent name used |
Delivery Lifecycle
As a message moves through the Unified Messaging pipeline, your webhook receives notifications at each stage. The overall delivery.status transitions through these states:
| Status | Meaning | Is Final? |
|---|---|---|
PENDING | Message accepted by EnableX, routing not yet started | No |
QUEUED | Message queued on a specific channel for delivery | No |
SENT | Message dispatched to the channel provider | No |
DELIVERED | Delivery confirmed to the recipient's device | Yes |
FAILED | All channels exhausted — message could not be delivered | Yes |
Statuses like PENDING, QUEUED, and SENT are transient — the message is still being processed and may cascade to the next channel. DELIVERED and FAILED are final. When you see FAILED on a channel sub-object but the overall status is still QUEUED, it means EnableX is attempting the next channel in the cascade.
Cascade Lifecycle Example
Consider a message sent with channels: ["rcs", "whatsapp", "sms"] where the recipient is not RCS-capable but has WhatsApp:
- Notification 1:
delivery.status: "QUEUED",delivery.channel: "rcs"— Message queued on RCS. - Notification 2:
delivery.rcs.status: "FAILED",delivery.status: "QUEUED",delivery.channel: "whatsapp"— RCS failed, cascading to WhatsApp. - Notification 3:
delivery.status: "DELIVERED",delivery.channel: "whatsapp",delivery.whatsapp.status: "DELIVERED"— Message delivered via WhatsApp. Final.
Building a Webhook Handler
Your webhook handler needs to process delivery notifications for all three channels through a single endpoint. The key fields to inspect are delivery.status (overall) and delivery.channel (which channel the update relates to).
Complete Handler Example
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/unified', (req, res) => {
// Always acknowledge immediately
res.sendStatus(200);
const { message_id, to, delivery } = req.body;
const { status, channel } = delivery;
switch (status) {
case 'QUEUED':
console.log(`[${message_id}] Queued on ${channel}`);
db.updateMessage(message_id, { channel, status: 'queued' });
break;
case 'SENT':
console.log(`[${message_id}] Sent via ${channel}`);
db.updateMessage(message_id, { channel, status: 'sent' });
break;
case 'DELIVERED':
console.log(`[${message_id}] Delivered via ${channel}`);
db.updateMessage(message_id, {
channel,
status: 'delivered',
delivered_at: delivery[channel]?.delivered
});
break;
case 'FAILED':
// Check if a specific channel failed (cascade continues)
// or if overall delivery failed (all channels exhausted)
const channelStatus = delivery[channel];
if (channelStatus?.status === 'FAILED') {
console.log(`[${message_id}] ${channel} failed: ${channelStatus.failure_reason}`);
}
// If overall status is FAILED, all channels are exhausted
console.log(`[${message_id}] All channels failed`);
alertOpsTeam(message_id, to);
break;
}
});
app.listen(3000, () => console.log('Webhook handler running on port 3000'));
from flask import Flask, request
import logging
app = Flask(__name__)
logger = logging.getLogger(__name__)
@app.route('/webhooks/unified', methods=['POST'])
def unified_webhook():
payload = request.get_json()
message_id = payload.get('message_id')
delivery = payload.get('delivery', {})
status = delivery.get('status')
channel = delivery.get('channel')
if status == 'QUEUED':
logger.info(f'[{message_id}] Queued on {channel}')
db.update_message(message_id, channel=channel, status='queued')
elif status == 'SENT':
logger.info(f'[{message_id}] Sent via {channel}')
db.update_message(message_id, channel=channel, status='sent')
elif status == 'DELIVERED':
logger.info(f'[{message_id}] Delivered via {channel}')
channel_data = delivery.get(channel, {})
db.update_message(
message_id,
channel=channel,
status='delivered',
delivered_at=channel_data.get('delivered')
)
elif status == 'FAILED':
logger.warning(f'[{message_id}] All channels failed')
alert_ops_team(message_id, payload.get('to'))
# Always acknowledge immediately
return '', 200
<?php
header("HTTP/1.1 200 OK");
$payload = json_decode(file_get_contents('php://input'), true);
$messageId = $payload['message_id'] ?? '';
$delivery = $payload['delivery'] ?? [];
$status = $delivery['status'] ?? '';
$channel = $delivery['channel'] ?? '';
switch ($status) {
case 'QUEUED':
error_log("[{$messageId}] Queued on {$channel}");
updateMessage($messageId, $channel, 'queued');
break;
case 'SENT':
error_log("[{$messageId}] Sent via {$channel}");
updateMessage($messageId, $channel, 'sent');
break;
case 'DELIVERED':
error_log("[{$messageId}] Delivered via {$channel}");
$deliveredAt = $delivery[$channel]['delivered'] ?? null;
updateMessage($messageId, $channel, 'delivered', $deliveredAt);
break;
case 'FAILED':
error_log("[{$messageId}] All channels failed");
alertOpsTeam($messageId, $payload['to'] ?? '');
break;
}
?>
Webhooks in Parallel Delivery Mode
When multi_channel_delivery is set to true, the message is sent across all channels simultaneously. In this mode, your webhook receives separate notifications for each channel independently. There is no cascade — each channel delivers (or fails) on its own.
Your handler should expect multiple notifications for the same message_id, one per channel. The overall delivery.status reflects the combined outcome: DELIVERED if at least one channel succeeds, FAILED only if all channels fail.
Incoming Messages
Incoming messages (replies from recipients) are delivered to the webhook URL configured for the specific channel — not the unified messaging webhook. For example, a WhatsApp reply goes to your WhatsApp webhook, an SMS reply to your SMS webhook, and an RCS reply to your RCS webhook.
This design means you can use a single unified webhook for all outbound delivery tracking, while each channel's inbound handling remains in its own endpoint with channel-specific payload formats.
For incoming message payload formats, refer to:
Troubleshooting
Not Receiving Webhooks
- Check your webhook URL — Ensure it is correctly configured in the EnableX Portal under Unified Messaging settings.
- Verify HTTPS — Your endpoint must use HTTPS with a valid (not self-signed) TLS/SSL certificate.
- Check firewall rules — Ensure your server accepts incoming POST requests from EnableX's IP ranges.
- Verify HTTP 200 response — If your server responds with anything other than
200, EnableX will retry and may eventually stop sending notifications.
Receiving Duplicate Notifications
Duplicate notifications can occur if your server does not return HTTP 200 quickly enough. Implement idempotent processing by tracking the message_id + delivery.status + delivery.channel combination to deduplicate.
Delayed Notifications
Delivery notifications depend on the underlying channel provider. WhatsApp and RCS typically deliver status updates within seconds. SMS delivery receipts may take longer depending on the carrier and destination country.
Understanding Cascade Notifications
In cascade mode, you may receive multiple notifications as the message moves between channels. Track the delivery.channel field to see which channel each update relates to. The overall delivery.status shows the combined state of the cascade.
What's Next
- Unified Messaging Guide — API reference, payload structure, channel routing, send variations
- SMS Guide — SMS-specific delivery receipts, incoming messages, DLT registration
- WhatsApp Webhooks — WhatsApp-specific webhook events and payload formats
- RCS Webhooks — RCS-specific webhook events and payload formats