Every event is identified by a type in the form <channel>.<domain>.<action>. The envelope’s data field changes based on the type.
WhatsApp — messages
whatsapp.message.received
Inbound message from a user to the WhatsApp channel.
{
"id": "evt_01HXYZ...",
"type": "whatsapp.message.received",
"occurred_at": "2026-04-27T12:34:56Z",
"company_id": "comp_xxx",
"channel_id": "ch_xxx",
"data": {
"phone_number_id": "1234567890",
"from": "+56912345678",
"to": "+56987654321",
"message_id": "wamid.HBgL...",
"type": "text",
"text": { "body": "Hola, quiero info" },
"timestamp": "1714214096"
}
}
| Field | Type | Description |
|---|
phone_number_id | string | ID of the sending Keebai number (our side). |
from | string | Sending user’s number (E.164). |
to | string | Your WhatsApp channel’s number. |
message_id | string | Unique wamid.XXX from Meta. Idempotency key. |
type | enum | text | image | audio | video | document | sticker | location | contacts | interactive. |
text | object | null | If type=text, contains { body }. |
timestamp | string | Meta Unix seconds. |
whatsapp.message.sent
Confirmation that the outbound message was accepted by Meta.
{
"type": "whatsapp.message.sent",
"data": {
"phone_number_id": "1234567890",
"message_id": "wamid.HBgL...",
"recipient_id": "+56912345678",
"status": "sent",
"timestamp": "1714214097"
}
}
whatsapp.message.delivered
The outbound message reached the user’s device (gray double-check in WhatsApp).
{
"type": "whatsapp.message.delivered",
"data": {
"phone_number_id": "1234567890",
"message_id": "wamid.HBgL...",
"recipient_id": "+56912345678",
"status": "delivered",
"timestamp": "1714214098"
}
}
whatsapp.message.read
The user opened the conversation and saw the message (blue double-check).
{
"type": "whatsapp.message.read",
"data": {
"phone_number_id": "1234567890",
"message_id": "wamid.HBgL...",
"recipient_id": "+56912345678",
"status": "read",
"timestamp": "1714214099"
}
}
whatsapp.message.failed
Meta rejected the send. Error info is available in data.error.
{
"type": "whatsapp.message.failed",
"data": {
"phone_number_id": "1234567890",
"message_id": "wamid.HBgL...",
"recipient_id": "+56912345678",
"status": "failed",
"timestamp": "1714214100",
"error": {
"code": 131026,
"title": "Receiver is incapable of receiving this message",
"details": "..."
}
}
}
WhatsApp — templates
whatsapp.template.status_updated
Meta changed a template’s status (approval, rejection, pause, deletion).
{
"type": "whatsapp.template.status_updated",
"data": {
"template_name": "promo_otoño_v3",
"template_language": "es_CL",
"status": "APPROVED",
"reason": null
}
}
| Field | Possible values |
|---|
status | PENDING | IN_REVIEW | APPROVED | REJECTED | PAUSED | DISABLED | IN_APPEAL |
reason | string | null. Meta’s reason when it rejects, pauses, or disables a template. |
WhatsApp — channels
whatsapp.channel.connected
A WhatsApp Business channel finished embedded signup. Useful for kicking off onboarding in your product.
{
"type": "whatsapp.channel.connected",
"data": {
"business_account_id": "1234567890",
"phone_number_id": "9876543210",
"phone_number": "+56987654321"
}
}
whatsapp.channel.disconnected
The channel was disconnected (manually from the portal or revoked by Meta).
{
"type": "whatsapp.channel.disconnected",
"data": {
"business_account_id": "1234567890",
"phone_number_id": "9876543210",
"phone_number": "+56987654321"
}
}
Synthetic events (_test: true)
When you run keebai webhooks test <id> --event ... or POST /v1/webhooks/:id/test, we dispatch a synthetic event with the same shape as the real one, but with data._test: true. Useful for validating your pipeline in staging without real traffic.
{
"type": "whatsapp.message.received",
"data": {
"phone_number_id": "synthetic",
"from": "+1 555 0100",
"to": "+1 555 0200",
"message_id": "wamid.synthetic-...",
"type": "text",
"text": { "body": "Test event from Keebai" },
"timestamp": "...",
"_test": true
}
}
In your handler, dedup by X-Keebai-Event-Id or body.id. Retries on timeout / 5xx arrive with the same id but a different X-Keebai-Delivery-Id.