Overview
DLR webhooks let your application receive message status changes without polling GET /v1/messages/:message_id.
Bar9 sends a webhook when an SMS message moves through delivery states such as sent, delivered, or failed. Each request is signed with your webhook signing secret so your server can verify that the event came from Bar9 and was not modified in transit.
Configure a webhook
Open the dashboard, go to Webhooks, and enter your HTTPS endpoint URL. The URL must use https://; local and private network hosts are rejected.
When you create a webhook, Bar9 shows the signing secret once. Store it securely. Existing secrets are only shown as a preview. If you lose the secret, rotate it from the same dashboard page.
You can also configure the endpoint through the authenticated dashboard API:
curl https://api.bar9.me/v1/webhooks/dlr \
-H "Content-Type: application/json" \
--cookie "$BAR9_SESSION_COOKIE" \
-X PUT \
-d '{
"url": "https://example.com/webhooks/bar9/dlr",
"enabled": true
}'Event request
Bar9 sends a POST request with JSON body and these headers:
| Header | Notes |
|---|---|
X-Bar9-Event-ID | Unique event id. Store it to make processing idempotent. |
X-Bar9-Event-Type | Event name, for example message.delivered. |
X-Bar9-Timestamp | Unix timestamp used in the signature. |
X-Bar9-Signature | HMAC-SHA256 signature with a v1= prefix. |
Content-Type | application/json. |
Example payload:
{
"id": "evt_...",
"type": "message.delivered",
"created_at": 1778407200,
"data": {
"id": "msg_...",
"to": "+213661000000",
"sender": "BAR9",
"type": "message",
"status": "delivered",
"provider_message_id": "abc123",
"segments": 1,
"cost_credits": 5,
"client_reference": "order-1001",
"created_at": 1778407000,
"queued_at": 1778407000,
"sent_at": 1778407100,
"delivered_at": 1778407200,
"failed_at": null,
"failure_reason": null
}
}Verify signatures
Build the signed payload as:
{X-Bar9-Timestamp}.{X-Bar9-Event-ID}.{raw request body}Compute HMAC-SHA256 with your webhook signing secret and compare it to X-Bar9-Signature after removing the v1= prefix. Use a constant-time comparison and reject timestamps outside a short window such as five minutes.
Node.js example:
import crypto from 'node:crypto';
function verifyBar9Webhook({ secret, timestamp, eventId, rawBody, signature }) {
const value = signature.startsWith('v1=') ? signature.slice(3) : signature;
const signedPayload = `${timestamp}.${eventId}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
const expectedBytes = Buffer.from(expected, 'hex');
const valueBytes = Buffer.from(value, 'hex');
if (expectedBytes.length !== valueBytes.length) return false;
return crypto.timingSafeEqual(expectedBytes, valueBytes);
}Handle retries and duplicates
Your endpoint should return a 2xx status only after it has accepted the event. Store X-Bar9-Event-ID and ignore duplicates so repeated deliveries do not trigger the same business action twice.
Process webhooks asynchronously when possible. Keep the endpoint fast, verify the signature before parsing business fields, and do not trust the event until verification succeeds.
Status values
| Status | Meaning |
|---|---|
sent | The SMS provider accepted or submitted the SMS. |
delivered | The provider reported successful delivery. |
failed | The provider reported failure or non-delivery. |
Use client_reference to connect a webhook event back to your own order, user, or workflow id.