WhatsApp Events
Every Cloud API webhook for a customer that’s been onboarded through TP-Signup is forwarded to your account-level Webhook URL (set under Settings → Webhook URL on the partner panel). Three categories:
- Onboarding —
whatsapp.waba_onboardedfires once when the customer completes embedded signup. Internal tokens stripped. - Status updates —
sent/delivered/read/failed/deletedfor every outbound message you send. - Inbound messages — text, media, location, interactive responses, and the other Cloud API message types.
WABA → customer mapping is done server-side at TP-Signup time. You only ever see events for customers under your account; cross-tenant leakage is impossible.
Headers
Every POST carries:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Source | splashifypro |
X-Webhook-Type | meta_raw for status & inbound; absent for onboarding events |
1. whatsapp.waba_onboarded
Fires once when the upstream onboarding completes for a customer.
The isv_name_token field that Meta includes in this event is
stripped before delivery — do not expect or rely on it.
{
"event": "WABA_ONBOARDED",
"source": "splashifypro",
"waba_id": "123456789012345",
"phone_number_id": "623925589026353",
"phone_number": "+919999999999"
}Once you receive this, the customer’s phone numbers are immediately available via the Phone Numbers endpoint.
2. Status updates (sent / delivered / read / failed / deleted)
These follow the standard WhatsApp Cloud API envelope. The full raw
Meta payload is forwarded as-is. Status events all share the same
top-level shape; the statuses[].status field tells you which it is.
sent
Fires when the upstream accepts your outbound for delivery. The
conversation.origin.type field reflects the session origin
(marketing / utility / authentication / service /
referral_conversion).
{
"object": "whatsapp_business_account",
"entry": [{
"id": "3130247400631305",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "91XXXXXXXXXX",
"phone_number_id": "623925589026353"
},
"statuses": [{
"id": "wamid.HBgM...",
"status": "sent",
"timestamp": "1655287862",
"recipient_id": "91XXXXXXXXXX",
"conversation": {
"id": "92d5c04d20c643078be036db3ac05026",
"expiration_timestamp": "1655372820",
"origin": { "type": "marketing" }
},
"pricing": {
"billable": true,
"pricing_model": "CBP",
"category": "marketing"
}
}]
},
"field": "messages"
}]
}]
}delivered / read
Same envelope, with statuses[].status set to delivered or read.
pricing may be absent on basic read callbacks. Use these to
update your message-tracker UI; the billing trigger on our side is
controlled by the per-partner deduction-trigger toggle (admin-side).
failed
{
"object": "whatsapp_business_account",
"entry": [{
"id": "1568505090181585",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": { "...": "..." },
"statuses": [{
"id": "wamid.HBgM...",
"status": "failed",
"timestamp": "1655287620",
"recipient_id": "91XXXXXXXXXX",
"errors": [{
"code": 131047,
"title": "Message failed to send because more than 24 hours have passed since the customer last replied to this number",
"href": "https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/"
}]
}]
},
"field": "messages"
}]
}]
}errors[] carries the Meta error code; cross-reference the official
error-codes table .
deleted
Light envelope (no conversation / pricing):
{
"object": "whatsapp_business_account",
"entry": [{
"id": "3130247400631305",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": { "...": "..." },
"statuses": [{
"id": "wamid.HBgM...",
"status": "deleted",
"timestamp": "1655287862",
"recipient_id": "91XXXXXXXXXX"
}]
},
"field": "messages"
}]
}]
}3. Inbound messages
Forwarded verbatim from Meta. entry[].changes[].value.messages[].type
tells you the kind:
type | Message contains |
|---|---|
text | text.body — plain text reply |
reaction | reaction.message_id + reaction.emoji (≤ 30 days old) |
image / video / audio / document / sticker | <type>.id (media handle) + mime_type + sha256 |
audio with voice: true | Voice note (mime_type: "audio/ogg; codecs=opus") |
location | location.latitude + location.longitude |
contacts | contacts[] with name + phones |
interactive (button_reply / list_reply) | The button or list item the user tapped |
button | Quick-reply payload from a template message |
order | Catalog product order; carries order.product_items[] |
system | Service notifications (e.g. user_changed_number) |
unknown | Unsupported type with an errors[] description |
Example — text message:
{
"object": "whatsapp_business_account",
"entry": [{
"id": "2427770783922677",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "91XXXXXXXXXX",
"phone_number_id": "586727755839684"
},
"contacts": [{
"profile": { "name": "Pinnacle" },
"wa_id": "91XXXXXXXXXX"
}],
"messages": [{
"from": "91XXXXXXXXXX",
"id": "wamid.HBgM...",
"timestamp": "1655526425",
"type": "text",
"text": { "body": "Test message" }
}]
},
"field": "messages"
}]
}]
}Example — interactive list reply with context:
{
"messages": [{
"context": { "from": "91XXXXXXXXXX", "id": "wamid.HBgM..." },
"from": "91XXXXXXXXXX",
"id": "wamid.HBgM...",
"timestamp": "1655538521",
"type": "interactive",
"interactive": {
"type": "list_reply",
"list_reply": { "id": "id_1", "title": "one" }
}
}]
}Example — click-to-WhatsApp ad referral:
{
"messages": [{
"referral": {
"source_url": "AD_OR_POST_FB_URL",
"source_id": "ADID",
"source_type": "ad",
"headline": "AD_TITLE",
"body": "AD_DESCRIPTION",
"media_type": "image",
"image_url": "RAW_IMAGE_URL",
"thumbnail_url":"RAW_THUMBNAIL_URL"
},
"from": "SENDER_PHONE",
"id": "wamid.ID",
"timestamp": "TIMESTAMP",
"type": "text",
"text": { "body": "BODY" }
}]
}Delivery semantics
- Fire-and-forget. Meta gets a 200 OK from us regardless of
what your endpoint returns. No retries on this surface — keep
your endpoint healthy or rely on idempotent processing keyed on
statuses[].id/messages[].id. - 10 s timeout. Acknowledge fast, process async.
- Order is not guaranteed. Multiple status callbacks for the
same
wamidcan arrive out of order. Always trust the lateststatuses[].timestamp. - Bandwidth. Media isn’t included inline — you receive the
Meta media
idandsha256. Use the WhatsApp media endpoints to download.
Node handler example
app.post("/webhooks/splashify", express.json(), async (req, res) => {
const body = req.body;
if (body.event === "WABA_ONBOARDED") {
await db.customers.markOnboarded(body.waba_id, body.phone_number_id);
return res.status(200).end();
}
for (const entry of body.entry ?? []) {
for (const ch of entry.changes ?? []) {
const v = ch.value ?? {};
for (const s of v.statuses ?? []) {
await db.outbound.updateStatus(s.id, s.status, s);
}
for (const m of v.messages ?? []) {
await db.inbound.persist({
from: m.from,
phone_number_id: v.metadata?.phone_number_id,
...m,
});
}
}
}
res.status(200).end();
});