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-event as the source of truth for the event type.
  • Verify x-coussema-signature with the signingSecret returned at creation.
  • Record message.delivered and message.failed in 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

HeaderValue
content-typeapplication/json
user-agentCoussema-Webhooks/1.0
x-coussema-delivery-iddelivery UUID
x-coussema-eventmessage.delivered
x-coussema-timestamp2026-04-09T10:15:30.000Z
x-coussema-signaturev1=<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);
}