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.

When Webhooks Fire

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

RequirementDetails
ProtocolHTTPS — all webhook posts use HTTPS. Your server must accept HTTPS requests.
TLS/SSL CertificateA valid TLS/SSL certificate must be installed. Self-signed certificates are not supported.
Request FormatJSON content is posted as raw body (not form-encoded). Your handler must read the raw request body.
ResponseYour server must respond with HTTP 200 in the same connection. Any other response code triggers a retry.
HTTP 200 Acknowledgement Is Mandatory

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.

Development Testing

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:

  1. Top level — Message identifiers (message_id, to), the channels list, and the preference mode.
  2. 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

FieldTypeDescription
message_idStringUnique message identifier from the send response
toStringRecipient phone number (E.164)
channelsArrayChannels in resolved order. If preference was budget or priority, this may differ from the original request.
preferenceStringThe preference mode used: order, budget, or priority
delivery.statusStringOverall delivery status: PENDING, QUEUED, SENT, DELIVERED, or FAILED
delivery.channelStringThe 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:

FieldTypeDescription
message_idStringChannel-specific message identifier
statusStringChannel delivery status: PENDING, QUEUED, SENT, DELIVERED, FAILED
queuedStringTimestamp when the message was queued on this channel
sentStringTimestamp when the message was sent to the provider
deliveredStringTimestamp when delivery was confirmed
failure_reasonStringReason for failure (if applicable)

Channel-Specific Additional Fields

ChannelAdditional FieldsDescription
SMSsenderSender ID or phone number used
WhatsApprecipient_id, business_phone, conversationRecipient number, business number, and conversation object (with id and type: UIC or BIC)
RCSevent_type, message_status, agentRCS-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:

StatusMeaningIs Final?
PENDINGMessage accepted by EnableX, routing not yet startedNo
QUEUEDMessage queued on a specific channel for deliveryNo
SENTMessage dispatched to the channel providerNo
DELIVEREDDelivery confirmed to the recipient's deviceYes
FAILEDAll channels exhausted — message could not be deliveredYes
Transient vs Final Status

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:

  1. Notification 1: delivery.status: "QUEUED", delivery.channel: "rcs" — Message queued on RCS.
  2. Notification 2: delivery.rcs.status: "FAILED", delivery.status: "QUEUED", delivery.channel: "whatsapp" — RCS failed, cascading to WhatsApp.
  3. 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 Use Channel-Specific Webhooks

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