Skip to main content

Webhook events

Every event Ponitor sends, and what's in its data. Team webhooks receive the events you pick. Sender webhooks receive message.received and message.status for their sender.

For delivery and signing, see Receiving webhooks. New events and fields may be added. Ignore any you don't recognise.

Team webhook events​

Messages​

EventWhen
message.sentThe network accepted an SMS or WhatsApp message.
message.deliveredThe network confirmed it reached the phone.
message.failedThe message wasn't delivered. data.status is EXPIRED, UNDELIVERED or FAILED, and failureCode and failedReason say why (Errors).
message.receivedA customer replied to your Short Code or WhatsApp number. Only this event includes the message content, with contact and replyTo as in Sender webhook events.

A message that ends as UNKNOWN sends no event. It may have arrived. See Delivery statuses.

Every message, campaign and airtime event's data has workspace: { "id", "handle" } of the workspace it belongs to, or null for Main.

{
"id": "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
"type": "message.failed",
"createdAt": "2026-10-05T09:31:12.000Z",
"teamId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"channel": "SMS",
"direction": "OUTBOUND",
"sender": "ACME",
"recipient": "+231770000000",
"status": "EXPIRED",
"failedReason": "Recipient unreachable: phone was off or out of coverage until the message expired",
"failureCode": "E11001",
"externalId": "order-42",
"workspace": null,
"createdAt": "2026-10-05T09:30:00.000Z"
}
}

Campaigns​

EventWhen
campaign.startedA campaign started sending to its group or list.
campaign.failedA campaign couldn't start, and nothing was sent. error says why.

data has id, name, groupId or listId, sender, batchId and error. groupId is set for a contact group and listId for a saved list. The other is null. batchId is the send the campaign started, or null.

Sender IDs and USSD codes​

EventWhen
sender_id.approvedA Sender ID or Short Code you requested was approved.
sender_id.rejectedA Sender ID or Short Code was rejected. reason says why.
ussd_code.approvedA USSD code you requested was approved.
ussd_code.rejectedA USSD code was rejected. reason says why.
{
"type": "sender_id.approved",
"data": {
"requestId": "1b7e4f2c-0d6a-4c51-9a83-2f6e1c0b9d47",
"productType": "SMS_SENDER_ID",
"status": "APPROVED",
"reason": null,
"senderId": "ACME",
"shortcodes": null,
"serviceCodes": null
}
}

productType is SMS_SENDER_ID, TWO_WAY_SMS_SHORTCODE or USSD_SERVICE_CODE. The codes are in senderId, shortcodes or serviceCodes to match.

Airtime​

EventWhen
airtime.purchase.deliveredA purchase of airtime or a data bundle succeeded. This covers purchases from the API and from the app.
airtime.purchase.failedA purchase failed. Its cost was refunded to your account.
airtime.batch.finishedA bulk purchase from the app finished, was stopped, or failed.
airtime.schedule.skippedA scheduled bulk purchase didn't run, for example because the balance was too low.

A data bundle purchase that failed:

{
"type": "airtime.purchase.failed",
"data": {
"id": "6bdffde4-b7e7-4950-881f-44620c2da9af",
"externalId": "order-8844",
"batchId": null,
"workspace": null,
"msisdn": "231880000001",
"carrier": "LONESTAR",
"amount": 9,
"cost": 9,
"status": "FAILED",
"failureReason": "Already purchased",
"refunded": true,
"createdAt": "2026-10-20T19:41:12.000Z",
"type": "DATA",
"product": {
"id": "prod_p4nzx9xp335h",
"code": "mtn-data-15gb-30d",
"name": "Lonestar Cell MTN 15GB, 30 days",
"data": "15GB",
"validity": "30 days",
"price": 9,
"currency": "USD"
},
"instructions": null
}
}

What a purchase event's data holds:

FieldWhat it is
typeAIRTIME or DATA. This isn't the event's own type, such as airtime.purchase.delivered.
externalIdYour reference, for a purchase made through the API. Null for a purchase made in the app.
batchIdThe bulk purchase it was part of. Null for a single purchase.
amount, costWhat you paid. For airtime the two are equal. For a data bundle both are the bundle's price. No fees are added. See Airtime and data bundles.
productFor a data bundle, the bundle as it was when bought: its ID, code, name, data allowance, validity, price and currency. Null for airtime.
instructionsText from the network for the subscriber, such as how to use a bundle. Usually null. Pass it on when it's set.
failureReason, refundedOn a failed purchase, why it failed and whether its cost went back to your account.

What airtime.batch.finished holds:

FieldWhat it is
id, labelThe bulk purchase and the name you gave it.
status, errorCOMPLETED, CANCELLED or FAILED, and on a failure, why.
scheduleIdThe schedule that started it. Null if you started it yourself.
total, sent, failed, skipped, pendingHow many numbers ended up in each state. Each number still pending sends its own purchase event when it settles.
reservedAmount, spentAmount, refundedAmountWhat was held when it started, what was spent, and what went back to your account.
createdAt, completedAtWhen it started and when it finished.

What airtime.schedule.skipped holds:

FieldWhat it is
scheduleId, nameThe schedule.
scheduledForWhen it was meant to run.
status, reasonSKIPPED or FAILED, and why.
pausedTrue if the schedule stopped itself. It won't run again until you resume it.
nextRunAtWhen it runs next, or null while it's paused.

Account​

EventWhendata
wallet.topped_upMoney was added to your account.amount, balance, source (top_up or invoice), reference
wallet.low_balanceYour balance fell below your low-balance alert.balance, thresholdUsd
wallet.overdraft_limit_reachedThe account reached its overdraft limit, so sending stopped.balance, overdraftFloorUsd

Amounts are in US dollars. The event names use wallet, an older name for your account. The names won't change.

Sender webhook events​

A sender webhook receives these two events for its own sender. The envelope has senderId and channel instead of teamId.

EventWhendata
message.receivedA customer replied to this sender.The reply, who sent it and to which number. See its fields.
message.statusOne of this sender's messages changed status, from QUEUED when you send it to its final status.id, externalId, status
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.received",
"createdAt": "2026-10-05T09:40:00.000Z",
"senderId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"channel": "SMS",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"sender": "+231770001234",
"recipient": "4455",
"conversationId": "9b2e7c41-0d3a-4f6e-8a51-2c7d9e0b6f13",
"contact": { "id": null, "name": null, "whatsapp": null },
"replyTo": null,
"createdAt": "2026-10-05T09:40:00.000Z",
"text": "Yes, please deliver tomorrow",
"content": "Yes, please deliver tomorrow",
"externalId": null,
"status": "DELIVERED"
}
}

The data when a WhatsApp customer taps a button on your message:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"sender": "+231770123456",
"recipient": "+231770000000",
"conversationId": "9b2e7c41-0d3a-4f6e-8a51-2c7d9e0b6f13",
"contact": {
"id": "0f3a9c2e-5b7d-4e1a-8c6f-2d9b3a4e5f60",
"name": "Musu Kollie",
"whatsapp": {
"name": "Musu",
"username": null,
"phoneNumber": "+231770123456",
"userId": "LR.00000000000000000000"
}
},
"replyTo": "6f1c2a3e-8b4d-4c1a-9e2f-3a4b5c6d7e8f",
"createdAt": "2026-10-05T09:41:00.000Z",
"text": "Yes",
"reply": { "id": "yes", "title": "Yes" },
"content": { "type": "text", "text": { "body": "Yes" }, "button": { "text": "Yes", "payload": "yes" } },
"externalId": null,
"status": "DELIVERED"
}

message.received fields​

FieldWhat it is
id, createdAtPonitor's ID for the reply, and when it arrived.
senderThe customer who replied, not your Sender ID.
recipientYour Short Code or WhatsApp number.
conversationIdThe same for every message between you and this customer on this number.
contactWho the customer is. id and name are your saved contact for their number, or null if you haven't saved them. whatsapp is what WhatsApp says about them, and null on SMS.
replyToPonitor's ID for your message, when the customer replied to it. Otherwise null.
text, image, video, …The message, in the field named after its type, as in Send a message. That's text, image, video, audio, document, sticker, location, contacts or order. A media field's url works for an hour.
replySet when the customer tapped a button or quick reply, or picked a list row. It has the id you sent, the title they saw, and a row's description. Match on id, which stays the same when the words change.
referralSet when the customer wrote from your click-to-WhatsApp ad. It has the ad's headline, body and sourceUrl.
forwardedFORWARDED or FREQUENTLY_FORWARDED when the customer forwarded the message.
contentDeprecated. The same message in the older shape. It's still sent so code written against it keeps working. Read text or the field named after the type in new code.
externalId, statusFor a reply, always null and DELIVERED.

Team webhooks use the same names.

On WhatsApp, contact.whatsapp has the name and username the customer set, their phoneNumber, and WhatsApp's userId for them. WhatsApp can hide a customer's number if they set a username and haven't written to you recently. Then phoneNumber is null, and sender is the ID WhatsApp gives them for your business, such as LR.00000000000000000000: two letters for their country, a dot, then letters and digits. Use it as the recipient in Send a message. When their number shows again, their messages come from it.

To mark a message read and show "typing…", use Mark a message read.

To see why a message wasn't delivered, fetch it with Get a message and read failureCode and failedReason.

webhook.ping​

Checks that your endpoint is reachable. It's sent from Send Test Event on a team webhook, and when you save a sender webhook. data is empty. Return 2xx.