Skip to main content

Webhooks and Delivery Notifications

There are two ways to track message delivery: polling and webhooks. Choose the approach that best fits your use case.

Polling​

Query the delivery status on demand:

GET /messages/status/{customerMessageId}?channel=SMS

channel is required: SMS, RCS or WHATSAPP. Or check multiple messages of the same channel at once:

GET /messages/status?channel=SMS&ids=id1,id2,id3

Both return the message's deliveryStatus (DELIVERED, UNDELIVERABLE, EXPIRED, ...): the Delivery Statuses table lists every value.

When to use polling:

  • Low message volume
  • Ad-hoc status checks
  • Simple integrations that don't need a public endpoint

Webhooks​

Register an HTTPS callback URL to receive delivery status updates in real time, as soon as the carrier reports them.

Configure a webhook​

POST /webhooks/delivery-status

Provide your callback URL as {"callbackUrl": "https://..."}. The same URL receives the events of SMS, RCS, and WhatsApp. The API will send HTTP POST requests to this URL when a delivery receipt arrives, when a recipient reads an RCS or WhatsApp message, and when an end user writes to you.

Manage your webhook​

ActionEndpointResponse
Get current webhookGET /webhooks/delivery-status200 with companyId and callbackUrl, 404 if none
Create webhookPOST /webhooks/delivery-status201 with companyId and callbackUrl, 409 if one exists
Update webhook URLPUT /webhooks/delivery-status200 with companyId and callbackUrl, 404 if none
Revoke webhookDELETE /webhooks/delivery-status204 with no body, 404 if none
Info

Only one active delivery-status webhook is allowed per account. If one is already configured, POST answers 409 Conflict and leaves it unchanged: change the URL with PUT, or DELETE the webhook first.

Webhook flow​

Webhook payload​

When a delivery receipt arrives, the API sends a POST request to your callback URL. The body is JSON (Content-Type: application/json), and fields without a value are left out.

Delivery notification example (SMS):

{
"eventType": "DELIVERY",
"channel": "SMS",
"messageId": "order-12345",
"destination": "+393401234567",
"statusCode": 3,
"description": "delivered",
"eventDate": "2026-04-09T11:30:05+02:00",
"price": 0.035,
"totalParts": 1,
"numPart": 1
}

messageId is the id of the message you sent, the same value the polling API calls customerMessageId. An SMS produces one event per part (numPart of totalParts). RCS and WhatsApp DELIVERY events carry the same fields without totalParts and numPart.

Read notification example (WhatsApp/RCS only):

{
"eventType": "READ",
"channel": "WHATSAPP",
"messageId": "e76614d1-4ac1-4d94-89f0-d07f1b5a190c",
"destination": "+393401234567",
"eventDate": "2026-04-09T11:31:15+02:00"
}

A READ event has no statusCode: a read message stays DELIVERED.

Inbound message example (WhatsApp/RCS only):

{
"eventType": "INBOUND",
"channel": "WHATSAPP",
"messageId": "b7e2c9a4-3f1d-4c6e-8a5b-0d9f2e7c4a18",
"source": "+393401234567",
"destination": "+393409876543",
"receivedDate": "2026-04-09T11:35:00+02:00",
"messageType": "TEXT",
"text": "Yes, I confirm my appointment"
}

source is the end user's number and destination is your WhatsApp number (your agent id on RCS). messageId identifies the incoming message in the conversation. Media messages also carry a mediaKey.

The statusCode of a DELIVERY event, with its description:

Status CodeDescriptionChannelsMeaning
1acceptedSMSHanded to the carrier, no receipt yet. Never sent as an event.
2rejectedSMSThe carrier refused the message.
3deliveredSMS, RCS, WhatsAppThe message reached the handset.
4expiredSMS, RCS, WhatsAppNot delivered within its validity window.
5deletedSMSCancelled before delivery.
6undeliverableSMSThe destination cannot receive it (unreachable or invalid number).
9general errorRCS, WhatsAppDelivery failed.
10disabledRCS, WhatsAppThe recipient has the channel switched off.
11unsupportedRCS, WhatsAppThe device or number does not support the channel.
12conversation closedWhatsAppThe 24-hour customer-service window had closed.
0unknownSMS, RCS, WhatsAppNo status held for the message. Never sent as an event.

An event is sent only once the receipt arrives, so every code you receive is final. A message still in flight reads ACCEPTED on SMS and UNKNOWN on RCS and WhatsApp when you poll it, and produces no event. The codes match the deliveryStatus values of the polling API: see Delivery Statuses.

Testing webhooks locally​

To test webhooks during development, use ngrok to expose your local server:

# Start your local webhook handler on port 3000
node webhook-handler.js

# In another terminal, expose it via ngrok
ngrok http 3000

# Copy the HTTPS URL (e.g. https://abc123.ngrok.io)
# Register it as your webhook callback:
curl -X POST "https://api.qlara.ai/api/partner-gateway/v1/webhooks/delivery-status" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"callbackUrl": "https://abc123.ngrok.io/webhook"}'
Tip

simulation: true validates a send request without sending the message, so it produces no webhook callback. To test your handler end to end, send a real message to a number you own.

Example webhook handler (Node.js)​

webhook-handler.js
const express = require('express');
const app = express();
app.use(express.json());

// Final failures: SMS = rejected, deleted, undeliverable
// RCS/WhatsApp = general error, disabled, unsupported, conversation closed
const SMS_FAILURES = [2, 5, 6];
const RCS_WA_FAILURES = [9, 10, 11, 12];

app.post('/webhook', (req, res) => {
const { eventType, messageId, statusCode, channel } = req.body;

// Events can arrive more than once: deduplicate on eventType + messageId (+ numPart on SMS)

switch (eventType) {
case 'DELIVERY':
if (statusCode === 3) {
console.log(`Message ${messageId} delivered via ${channel}`);
// Update your database: mark message as delivered
} else if (statusCode === 4) {
console.log(`Message ${messageId} expired via ${channel}`);
} else if (SMS_FAILURES.includes(statusCode) || RCS_WA_FAILURES.includes(statusCode)) {
console.log(`Message ${messageId} not delivered via ${channel}: ${req.body.description}`);
// Handle failure: retry or notify user
}
break;

case 'READ':
console.log(`Message ${messageId} read by recipient at ${req.body.eventDate}`);
break;

case 'INBOUND':
console.log(`Inbound from ${req.body.source} at ${req.body.receivedDate}: ${req.body.text}`);
// Process reply: auto-respond, forward to CRM, etc.
break;
}

// Always respond quickly (within 5 seconds) — process asynchronously if needed
res.sendStatus(200);
});

app.listen(3000, () => console.log('Webhook handler listening on port 3000'));

Best practices​

  1. Use HTTPS on a public host -- The callback URL is validated when you register it. It must use https on port 443 (the default) or 8443, carry no credentials and no # fragment, and its host must resolve to publicly reachable addresses. Loopback, private, link-local, carrier-grade-NAT and unique-local addresses are refused, as is a host that does not resolve. A URL that fails any of these checks is rejected with 400.
  2. Respond quickly -- Answer with any 2xx within 5 seconds (3 seconds to connect). Process the payload asynchronously if needed.
  3. Handle retries -- If your endpoint is unreachable or does not answer 2xx, the system retries: up to 5 attempts in total, at least 5 minutes apart for SMS and at least 10 minutes apart for RCS and WhatsApp, and only for messages sent in the last 10 days. Events can arrive more than once and out of order, so make your processing idempotent, with eventType + messageId (+ numPart on SMS) as the key.
  4. Validate the payload -- Callbacks carry no signature header. Treat the body as untrusted input and check that the messageId of a DELIVERY or READ event belongs to a message you sent.
  5. Monitor failures -- If your webhook endpoint is consistently failing, check your server logs and ensure the URL is accessible. Events that exhaust their retries are not sent again: recover those statuses by polling.

Choosing between polling and webhooks​

AspectPollingWebhooks
LatencyDepends on poll frequencyNear real-time
ComplexitySimple GET requestsRequires a public HTTPS endpoint
ScalabilityIncreases API calls at scalePush-based, no extra API calls
Best forLow volume, ad-hoc checksHigh volume, real-time dashboards

See the Webhooks API Reference and Message Delivery Status API Reference for full endpoint documentation.