Notify Africa

Webhooks

Configure callbacks in the Portal, test delivery, verify signatures, and process WhatsApp events safely.

Developer webhooks deliver normalized WhatsApp events from Notify Africa to your HTTPS endpoint.

Meta.
Notify Africa.
Your webhook endpoint.

Configuration Boundary

Configure webhooks in the Notify Africa Portal. The Portal owns:

  • endpoint creation and URL changes.
  • event subscriptions.
  • pause/reactivation and deletion.
  • signing-secret creation and rotation.
  • delivery-attempt inspection.
  • queuing a test delivery.

Webhook management has no Developer API (API-key) surface. Every webhook action, including queuing a test delivery, requires an authenticated Portal session.

Configure in the Portal

  1. Add an HTTPS callback URL.
  2. Select one or more supported event types.
  3. Save the generated whsec_... secret immediately.
  4. Keep the endpoint active.
  5. Queue a test delivery directly in the Portal.
  6. Inspect failures and endpoint health in the Portal.

An empty subscription receives no production events. Creating or reactivating an endpoint requires a ready WABA connection.

Supported Events

EventAggregateTrigger
message.receivedmessageInbound customer message
message.status.updatedmessageOutbound/inbound message status change
template.status.updatedtemplateProvider template lifecycle change

Test Webhook

Once an endpoint is active, queue a test delivery from the Portal to confirm your receiver is reachable and verifying signatures correctly, before relying on it for real WhatsApp events.

  1. Open the webhook endpoint in the Portal.
  2. Trigger a test delivery for that endpoint.
  3. Notify Africa sends one sample message.received event to your callback URL, signed exactly like a real event.
  4. Confirm your receiver logged the incoming request.
  5. Confirm the signature verified — the same check your receiver runs on real events.
  6. Check the Portal for the attempt's outcome (delivered, failed, or retrying) and the response your endpoint returned.

The endpoint must be active before you can queue a test.

Your receiver gets the same envelope as a real callback, with fabricated content — no real message exists behind aggregateId: "test":

{
  "eventId": "7c61520b-7c77-4df6-989a-3b784d246f66",
  "authUserId": 12345,
  "eventType": "message.received",
  "aggregateType": "message",
  "aggregateId": "test",
  "dedupeKey": "test:2a3fbc46-5589-4f91-9e4b-31ed5c89f141:7c61520b-7c77-4df6-989a-3b784d246f66",
  "occurredAt": "2026-09-09T10:40:00.000Z",
  "data": {
    "test": true,
    "message": "This is a test delivery from Notify."
  }
}

Next: return HTTP 2xx after durably accepting the callback.

Callback Request

Notify Africa sends:

POST /your-callback-path HTTP/1.1
Content-Type: application/json
x-notify-webhook-timestamp: 1788942600
x-notify-webhook-signature: sha256=...

The body uses this envelope:

FieldTypeMeaning
eventIdUUID stringUnique event identifier; primary idempotency key for receivers
authUserIdnumberNotify Africa account-owner identifier
eventTypestringSubscribed event name
aggregateTypemessage or templateResource family
aggregateIdstringIdentifier chosen for the affected resource
dedupeKeystringNotify Africa event-production deduplication key
occurredAtISO-8601 stringDomain-event occurrence time
dataobjectEvent-specific data

Message Received Payload

{
  "eventId": "bd8e8c07-3630-4d28-8660-82cf5cb1a5e6",
  "authUserId": 12345,
  "eventType": "message.received",
  "aggregateType": "message",
  "aggregateId": "501",
  "dedupeKey": "developer-webhook:waba:message:received:wamid-example",
  "occurredAt": "2026-09-09T10:45:00.000Z",
  "data": {
    "conversationId": 77,
    "messageId": 501,
    "wamid": "wamid.HBgLMjU1NzAwMDAwMDAxFQIAERgS...",
    "customerPhoneNumber": "255700000001",
    "customerName": "Jane",
    "direction": "inbound",
    "messageType": "text",
    "sentAt": "2026-09-09T10:44:59.000Z",
    "content": {
      "text": "Hello"
    }
  }
}

Message-received data can include conversationId, messageId, wamid, customerPhoneNumber, customerName, direction, messageType, sentAt, and normalized content.

Message Status Payload

{
  "eventId": "7c61520b-7c77-4df6-989a-3b784d246f66",
  "authUserId": 12345,
  "eventType": "message.status.updated",
  "aggregateType": "message",
  "aggregateId": "d9b54c80-8a63-4f8b-83c8-71f2439f5429",
  "dedupeKey": "developer-webhook:waba:message:status:example:delivered",
  "occurredAt": "2026-09-09T10:46:00.000Z",
  "data": {
    "conversationId": 77,
    "messageId": 501,
    "wamid": "wamid.HBgLMjU1NzAwMDAwMDAxFQIAERgS...",
    "customerPhoneNumber": "255700000001",
    "customerName": "Jane",
    "direction": "outbound",
    "messageType": "template",
    "status": "delivered",
    "recipientId": "255700000001",
    "providerTimestamp": "1788942760",
    "errors": [],
    "errorCode": null,
    "errorMessage": null,
    "retryable": false
  }
}

Status data can include provider errors and normalized errorCode, errorCategory, errorMessage, and retryable fields.

Template Status Payload

{
  "eventId": "4ac9ed86-8f75-4b02-bf89-a357d66ab193",
  "authUserId": 12345,
  "eventType": "template.status.updated",
  "aggregateType": "template",
  "aggregateId": "order_update:en",
  "dedupeKey": "developer-webhook:waba:template:status:provider-event-1",
  "occurredAt": "2026-09-09T10:47:00.000Z",
  "data": {
    "wabaId": "9876543210",
    "authUserId": 12345,
    "externalTemplateId": "7788990011",
    "externalTemplateName": "order_update",
    "externalTemplateLanguage": "en",
    "providerLanguage": "en",
    "externalTemplateCategory": "UTILITY",
    "parameterFormat": "positional",
    "providerStatus": "APPROVED",
    "status": "approved",
    "providerComponents": [],
    "rejectionReason": null,
    "rejectionInfo": null,
    "disableInfo": null,
    "lockInfo": null,
    "syncStatus": "synced",
    "syncedAt": "2026-09-09T10:47:00.000Z",
    "lastSyncError": null,
    "deletedAt": null,
    "revision": 3
  }
}

The normalized template event status can be pending, approved, rejected, or deleted. The template REST status vocabulary excludes deleted because deleted records are no longer an active retrievable resource.

Verify Signatures

Signature base string:

<x-notify-webhook-timestamp>.<raw-json-body>

Algorithm: HMAC-SHA256 using the Portal-generated webhook secret. Compare against the hexadecimal digest in x-notify-webhook-signature after the sha256= prefix.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyNotifyWebhook(input: {
  rawBody: Buffer;
  timestamp: string;
  signature: string;
  secret: string;
}): boolean {
  const expected = Buffer.from(
    'sha256=' +
      createHmac('sha256', input.secret)
        .update(input.timestamp)
        .update('.')
        .update(input.rawBody)
        .digest('hex'),
  );
  const received = Buffer.from(input.signature);

  return (
    expected.length === received.length &&
    timingSafeEqual(expected, received)
  );
}

Verify before parsing JSON. Reject missing signatures and timestamps. Enforce an application replay window, such as five minutes, before processing the event.

Idempotent Processing

Delivery is at least once. Duplicate and out-of-order callbacks are possible.

  1. Verify the signature and timestamp.
  2. Start a database transaction.
  3. Insert eventId into a table with a unique constraint.
  4. If the insert conflicts, return 2xx without repeating side effects.
  5. Apply the event state transition.
  6. Commit.
  7. Return 2xx.

Do not rely on arrival order. Compare occurredAt and domain state before replacing a newer local status.

Retry Behavior

OutcomeDelivery behavior
HTTP 2xxDelivered; no retry
Network error or timeoutRetry
408, 409, 425, 429Retry
5xxRetry
Other non-2xxDiscard

Current defaults:

  • callback timeout: 5 seconds.
  • retry window: 24 hours.
  • maximum attempts: 8.
  • full-jitter exponential delay from 1 second, capped at 5 minutes.
  • automatic pause after 10 consecutive failures.

Inspect pending, delivering, delivered, retrying, failed, and discarded attempts in the Portal.

URL Security

Webhook URLs must use HTTPS. Notify Africa rejects private, loopback, link-local, metadata, carrier-grade NAT, benchmark, multicast, and reserved destinations, including unsafe IPv6 ranges and IPv4-mapped IPv6 addresses.

The dispatcher resolves DNS before sending, validates the resolved destination, pins that address for the request, and does not follow redirects.

On this page