Webhooks
Create an endpoint, verify the signature and handle delivery events with the REST API. The public npm SDK is not available yet.
Signature
Callbacks use an HMAC-SHA256 signature over timestamp.payload. The timestamp and the signature arrive in the HTTP headers.
- Return a 2xx quickly once the callback is processed.
- Treat
x-coussema-eventas the source of truth for the event type. - Verify
x-coussema-signaturewith thesigningSecretreturned at creation. - Record
message.deliveredandmessage.failedin your business system.
POST /v1/webhooks
- message.queued
- message.sent
- message.delivered
- message.failed
javascript
const apiKey = process.env.COUSSEMA_API_KEY ?? '';
const response = await fetch('https://api.coussema.com/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/webhooks/coussema',
subscribed_events: ['message.delivered', 'message.failed'],
}),
});
const payload = await response.json();
if (!response.ok) throw new Error(`Webhook request failed: ${response.status}`);
console.log(payload.signingSecret);Headers
| Header | Value |
|---|---|
| content-type | application/json |
| user-agent | Coussema-Webhooks/1.0 |
| x-coussema-delivery-id | delivery UUID |
| x-coussema-event | message.delivered |
| x-coussema-timestamp | 2026-04-09T10:15:30.000Z |
| x-coussema-signature | v1=<hmac-sha256-hex> |
Payload
json
{
"organization_id": "2f0dd8f4-7b1c-4f38-b4c1-f4bc8aa6e8f4",
"message": {
"id": "a2f7e5cb-bbe9-4d7d-bd17-80d1d7a5e5af",
"campaign_id": null,
"recipient_phone": "+243810000000",
"sender_name": "Coussema",
"status": "delivered",
"source_surface": "api",
"sent_at": "2026-04-09T10:11:11.000Z",
"failed_at": null,
"delivered_at": "2026-04-09T10:13:45.000Z",
"failure": null
}
}Node.js verification
javascript
import { createHmac, timingSafeEqual } from 'node:crypto';
function handleWebhook(rawBody, headers) {
const timestamp = headers.get('x-coussema-timestamp');
const signature = headers.get('x-coussema-signature');
const secret = process.env.COUSSEMA_WEBHOOK_SECRET ?? '';
if (!timestamp || !signature || !secret) throw new Error('Missing signature data.');
const [version, incomingDigest = ''] = signature.split('=');
const timestampMs = Date.parse(timestamp);
const timestampIsFresh = Number.isFinite(timestampMs)
&& Math.abs(Date.now() - timestampMs) <= 5 * 60 * 1000;
const expectedDigest = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
const valid = timestampIsFresh
&& version === 'v1'
&& incomingDigest.length === expectedDigest.length
&& timingSafeEqual(Buffer.from(incomingDigest, 'utf8'), Buffer.from(expectedDigest, 'utf8'));
if (!valid) throw new Error('Invalid Coussema webhook signature.');
return JSON.parse(rawBody);
}