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
| Event | Sent when |
|---|---|
message.sent | A message was handed to the network. |
message.delivered | A message was delivered. |
message.failed | A message ended as undelivered, failed or expired. |
batch.completed | Every message in a batch has a final status. |
balance.low | Your balance fell below your alert threshold. |
sender_id.updated | A 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:
| Header | Value |
|---|---|
X-Signature | t=<timestamp>,v1=<signature> |
X-Webhook-Event | The event type. |
X-Webhook-Delivery | The 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.
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.