Skip to main content
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"
  }
}
FieldTypeDescription
phone_number_idstringID of the sending Keebai number (our side).
fromstringSending user’s number (E.164).
tostringYour WhatsApp channel’s number.
message_idstringUnique wamid.XXX from Meta. Idempotency key.
typeenumtext | image | audio | video | document | sticker | location | contacts | interactive.
textobject | nullIf type=text, contains { body }.
timestampstringMeta 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
  }
}
FieldPossible values
statusPENDING | IN_REVIEW | APPROVED | REJECTED | PAUSED | DISABLED | IN_APPEAL
reasonstring | 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.