Webhooks push real-time updates into your application as messages move through the network, payments confirm and customers reply. This page covers how to wire them up, every status code Sozuri returns synchronously, and the exact JSON payloads you’ll receive on each delivery callback.

Types of webhooks

WebhookFires when…
Message Status An outbound message changes state (queued → sent → delivered / read / failed). See delivery callbacks.
Received Message A customer sends a message to your shortcode or WhatsApp number. See 2-way SMS and WhatsApp notifications.
Payment An M-Pesa STK Push, C2B or Paybill transaction completes or fails. See payments.
Subscription A customer subscribes, unsubscribes or is billed on a Premium SMS service. See subscriptions.

Registering a callback URL

  1. Open your project dashboard.
  2. Go to Manage API › Callback URLs › Create new.
  3. Paste your HTTPS endpoint in the field for the event type you want (Status, Received Message, Payment, etc.).
  4. Save. Your webhook is active immediately — the next event will be delivered to your endpoint.
Your endpoint must accept anonymous POST requests and return a 2xx response promptly. Callbacks are not retried on non-2xx responses or timeouts, so keep your receiver highly available and idempotent.

Anatomy of a webhook request

  • Method — always POST
  • Content-Typeapplication/json
  • Body — JSON with the event payload (see each channel’s payload below)
  • messageId — the same ID returned in the API response when the message was sent
  • timestamp — Unix epoch in seconds. Always use this for ordering, not the order of arrival

Verifying authenticity

You can register an Auth Key per callback URL from the dashboard. Sozuri will include it in the body of every request so your server can confirm the call is genuine before processing it.

// Pseudocode
if (request.body.authKey !== process.env.SOZURI_CALLBACK_KEY) {
    return res.status(401).end();
}
// safe to process

Status codes (synchronous)

Every accepted message comes back with a recipients[].statusCode (numeric) and recipients[].status (string). Codes 11–14, 22 and 23 are returned in the immediate response. The rest (15–21) only appear later on your delivery webhook, never in the synchronous response.

Code Status Stage Channels Meaning
11acceptedSync acceptSMS, WhatsApp, Premium, AirtimeMessage received by Sozuri and queued for processing.
12unsupported_numberSync failSMSThe destination number is not supported on the chosen channel.
13queuedSync accept & DLRWhatsAppQueued at the carrier, awaiting delivery.
14scheduledSync acceptSMSDelivery has been scheduled for a future time.
15device_ackDLR onlySMSThe recipient’s device acknowledged delivery.
16readDLR onlyWhatsAppThe recipient read the message.
17expiredDLR onlySMSThe message could not be delivered within its validity window.
18stopped_by_userDLR onlySMSThe recipient opted out and the message was blocked.
19stopped_by_adminDLR onlySMSBlocked by carrier or platform policy.
21delivery_failureDLR onlySMSThe carrier reported a delivery failure.
22insufficient_balanceSync failSMSNot enough credits in the project to send the message.
23volume_limitSync failSMSThe project’s configured volume limit has been exceeded.
DLR-only codes arrive on your delivery webhook after the carrier confirms (or fails to confirm) handover to the device. Persist the latest statusCode per messageId to power dashboards and retries.

Response envelope per channel

Each channel wraps success and error payloads in a slightly different envelope — an artefact of how the underlying providers respond. The table below tells you which fields to parse for the message id, the status, and the error message. Channel-specific error responses are documented on each channel’s own page.

Channel Success envelope Error envelope Full error list
SMS messageData.messages, recipients[] messageData.message SMS errors
WhatsApp messageData.messages, recipients[] error · MessageData.Error · message + errors{} WhatsApp errors
Premium SMS (on-demand) messageData.messages, recipients[] status + description Premium on-demand errors
Premium SMS (subscription) messageData.messages, recipients[] status + description Subscription errors
Airtime data.messages, recipients[] data.message · status + errorMessage Airtime errors
Sample success response (SMS)
{
    "messageData": { "messages": 1 },
    "recipients": [
        {
            "messageId": "MSGBLK6012A7E8B90A21611835368",
            "to": "254700000001",
            "status": "accepted",
            "statusCode": "11",
            "messagePart": 1,
            "type": "bulk"
        }
    ]
}
Sample error response (SMS)
{
    "messageData": {
        "message": "Error. Insufficient balance. Top up and try again."
    }
}
Authentication errors (Project not found., Unknown project.) are shared across every channel and are documented on the Authentication page.

Delivery callbacks (asynchronous)

After Sozuri accepts a message, the final delivery outcome is pushed to your project’s Message Status callback URL. The following sections cover the exact JSON shape per channel and every status string we POST.

  1. Match callbacks to your records by messageId. Callbacks can arrive out of order; trust messageId + timestamp, not arrival order.
  2. Callbacks are not authenticated by default. Use the optional Auth Key (see above) and ignore any messageId you don’t recognise.
  3. No automatic retries. If your endpoint returns a non-2xx or times out, the callback is logged on our side but is not retried.

SMS delivery webhook

Identical shape for Safaricom and Airtel — the only difference is the network field and the values of status.

Payload
POST https://yourdomain.com/your/callback HTTP/1.1
Content-Type: application/json
Accept: application/json

{
    "project": "yourproject_name",
    "messageId": "MSGBLK5F96B6A0CC2EB1603712672",
    "channel": "sms",
    "status": "success",
    "network": "safaricom",
    "type": "bulkDelivery",
    "timestamp": 1603713484
}
Safaricom status values

Safaricom delivery reports are normalised to five strings. The carrier-side deliveryStatus is mapped before reaching your webhook.

statusCarrier codes (input)Meaning
success0Delivered to the recipient’s device.
network_failure1001Network error on the carrier side. Often transient — retry.
delivery_impossible1, 5, 11, 13, 21, 31, 32, 34, 99Permanent failure — barred number, terminal incompatible, blocked content, etc.
absent_subscriber6, 27Subscriber unreachable (off-network, off-coverage). Will not be retried by the carrier.
unknown_erroranything elseCarrier returned a code we don’t yet map. Treat as a permanent failure.
Airtel status values

Airtel delivery strings are forwarded as-is from the carrier — no normalisation is applied. The most common values observed in production:

statusMeaning
deliveredDelivered to the recipient’s device.
failedCarrier reported a delivery failure. Inspect description/status_code in your records for the reason.
carrier-specific stringsOther strings may appear depending on the Airtel response. Treat unknown values as permanent failures and log them so we can add a mapping.

WhatsApp delivery webhook

WhatsApp callbacks fire on every state change reported by Meta. The payload nests the delivery state under a statuses object that mirrors WhatsApp’s own structure.

Payload (successful state change)
{
    "project": "yourproject_name",
    "channel": "whatsapp",
    "phone_number": "254101523135",
    "statuses": {
        "recipient": "254725164293",
        "message_id": "wamid.HBgMMjU0NzI1MTY0Mjkz...",
        "status": "delivered",
        "type": "status",
        "conversation_id": "f659b9ba4d9551c39ca61498126da29f",
        "expiration_timestamp": "1692896813",
        "category": "user_initiated"
    }
}
Payload (failure)

When a message fails, the statuses object replaces conversation metadata with an error string sourced from Meta:

{
    "project": "yourproject_name",
    "channel": "whatsapp",
    "phone_number": "254101523135",
    "statuses": {
        "recipient": "254725164293",
        "message_id": "wamid.HBgMMjU0NzI1MTY0Mjkz...",
        "status": "failed",
        "type": "status",
        "error": "Re-engagement message"
    }
}
WhatsApp status values
statusMeaning
sentAccepted by WhatsApp servers.
deliveredDelivered to the recipient’s device.
readThe recipient opened the message. Only fires when the user has read receipts enabled.
failedPermanent failure. The error field carries Meta’s reason (e.g. Re-engagement message for messages outside the 24-hour customer-care window).

Premium SMS callbacks

Premium SMS uses dedicated callback types for subscriber lifecycle events on top of message delivery. The four event types you can receive:

EventWhen it fires
activateA subscriber opted in via your shortcode (texted the keyword).
deactivateA subscriber unsubscribed.
consentCarrier-side consent confirmation captured.
deliveryDelivery report for a premium message.

The full Premium callback payloads — including SubscriberLifeCycle field shapes — live on the Subscription premium SMS page.

Cross-channel mapping

If you handle more than one channel, this table lets you normalise the incoming string to a single internal vocabulary:

Conceptual stateSMS — SafaricomSMS — AirtelWhatsApp
Deliveredsuccessdelivereddelivered
Readread
Sent to network, no DLR yetsent
Transient failure (retryable)network_failure
Permanent failuredelivery_impossible, unknown_errorfailedfailed
Recipient unreachableabsent_subscriber(varies)(carried in error)

Use cases

The most common ways teams put webhooks and status codes to work.

Mark an order as paid

When the payment webhook fires with status: success, flip the order to paid and trigger fulfilment automatically.

Idempotent delivery receivers

Key your handler on messageId and apply changes idempotently. Late or duplicate callbacks become safe by construction.

Real-time delivery dashboard

Stream callbacks into your warehouse and chart delivery rate, network breakdown and failure reasons by hour. The network field gives you Safaricom vs Airtel splits for free.

Smart retries

Re-send only messages with transient failures (network_failure, 17 expired) and skip permanent ones (delivery_impossible, 18 stopped_by_user) automatically.

Wire Sozuri into your stack.

Configure a callback URL on your project, send a test message, and watch the events arrive in real time.