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.
All configuration operations require an authenticated dashboard session and approved production access.
GET /v1/webhooks/dlr
Read the current configuration:
curl https://api.bar9.dev/v1/webhooks/dlr \
--cookie "$BAR9_SESSION_COOKIE"
When no webhook is configured, the endpoint returns 200 OK with "data": null. Existing configurations return id, url, enabled, secret_preview, creation/update timestamps, and rotated_at. The full signing secret is never returned by this operation.
PUT /v1/webhooks/dlr
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.dev/v1/webhooks/dlr \
-H "Content-Type: application/json" \
--cookie "$BAR9_SESSION_COOKIE" \
-X PUT \
-d '{
"url": "https://example.com/webhooks/bar9/dlr",
"enabled": true
}'
The response is 200 OK. Creating a configuration returns signing_secret once. Updating an existing configuration keeps its current secret and returns only secret_preview.
{
"ok": true,
"data": {
"id": "wh_...",
"url": "https://example.com/webhooks/bar9/dlr",
"enabled": true,
"secret_preview": "whsec_••••••••a1b2",
"signing_secret": "whsec_...",
"created_at": 1778407200,
"updated_at": 1778407200,
"rotated_at": null
}
}
POST /v1/webhooks/dlr/rotate-secret
Rotate a configured secret:
curl https://api.bar9.dev/v1/webhooks/dlr/rotate-secret \
--cookie "$BAR9_SESSION_COOKIE" \
-X POST
The new signing_secret is returned once and the previous secret stops validating new deliveries immediately. An account without a configured webhook receives 404.
DELETE /v1/webhooks/dlr
Delete the current configuration:
curl https://api.bar9.dev/v1/webhooks/dlr \
--cookie "$BAR9_SESSION_COOKIE" \
-X DELETE
Success returns 204 No Content. Deleting a missing configuration is idempotent.
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": "+213****0000",
"sender": "BAR9",
"type": "message",
"status": "delivered",
"provider_message_id": "abc123",
"segments": 1,
"cost_credits": 5,
"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.
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
Return a 2xx status only after accepting the event. Store X-Bar9-Event-ID and ignore duplicates. 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.
Common errors
| Status | Typical cause |
|---|---|
400 |
Invalid URL, non-HTTPS URL, or private/local destination. |
401 |
Missing or invalid dashboard session. |
403 |
Production access has not been approved. |
404 |
Secret rotation requested before a webhook has been configured. |