Webhooks

Get notified about delivery and account events.

Webhooks send events to your server as they happen, so you don't have to keep asking for updates.

Register an endpoint

curl https://api.appstractone.com/v1/webhooks \
  -H "Authorization: Bearer $APPSTRACT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/appstract",
    "events": ["message.delivered", "message.failed", "batch.completed"],
    "description": "Order notifications"
  }'

The response includes a signing secret that starts with whsec_. It is shown only once, so store it securely. You can have up to 10 endpoints, and each must use HTTPS.

Events

EventSent when
message.sentA message was handed to the network.
message.deliveredA message was delivered.
message.failedA message ended as undelivered, failed or expired.
batch.completedEvery message in a batch has a final status.
balance.lowYour balance fell below your alert threshold.
sender_id.updatedA sender ID was approved or rejected.

See Webhook events for each payload.

What we send

Each event is a POST with a JSON body:

{
  "id": "evt_01M6MK2Q8YV4J3Z6Q1H0T7R5PA",
  "type": "message.delivered",
  "createdAt": "2026-11-05T00:48:01.902Z",
  "data": {
    "id": "msg_01M6MJSBV7KXZGYK5TZGGA0P6R",
    "to": "+233241234567",
    "from": "MYSHOP",
    "status": "delivered",
    "segments": 1,
    "reference": "order-1042",
    "batchId": null,
    "errorCode": null,
    "refunded": false
  }
}

With these headers:

HeaderValue
X-Signaturet=<timestamp>,v1=<signature>
X-Webhook-EventThe event type.
X-Webhook-DeliveryThe delivery ID.

Verify the signature

Check every request before trusting it. The signature is an HMAC-SHA256 of <timestamp>.<raw body>, using your signing secret, encoded as hex.

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

export function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map(part => part.split('=')));
  const timestamp = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}

Use the raw request body exactly as received. Parsing and re-encoding the JSON changes it and breaks the signature.

Respond quickly

Return any 2xx status within 10 seconds. Do slow work after you respond.

Retries

If your endpoint does not return 2xx, we retry after 10 seconds, doubling the wait each time up to once an hour, for 24 hours. After that the delivery is marked failed.

You can see every delivery and its attempts with GET /webhooks/{id}/deliveries, and send one again with the replay endpoint.

Events can arrive more than once and out of order. Use the event id to ignore duplicates.

On this page