Sign inGet Started

WhatsApp message lifecycle webhooks

Each step in a message's lifecycle can be pushed to your endpoint as it happens. Every payload uses the WhatsApp event envelope and carries the fields every message event carries.

Delivery events

whatsapp.accepted, whatsapp.sent, whatsapp.delivered, and whatsapp.read carry only the fields every message event carries. Lifecycle events gives what each one means and when whatsapp.delivered is skipped.
Contoh kode
{
  "data": {
    "direction": "outbound",
    "from": { "phone_number": "+13124495569" },
    "metadata": { "session_id": "sess_4821" },
    "tags": [{ "name": "flow", "value": "login-otp" }],
    "to": { "phone_number": "+14155550100" },
    "whatsapp_id": "wam_01ky7qbvswf3fvyaw3az90391c",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
  },
  "timestamp": "2026-07-23T14:51:39.913Z",
  "type": "whatsapp.delivered"
}
whatsapp.failed and whatsapp.rejected also carry an error object with a stable Bird code, a human-readable description, an optional meta_error_code, and occurred_at. See failure events for what separates the two. On a failure WhatsApp reported, description is WhatsApp's own explanation. A service message whose customer service window closed between accept and dispatch fails like this:
Contoh kode
{
  "data": {
    "direction": "outbound",
    "error": {
      "code": "service_window_expired",
      "description": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
      "meta_error_code": "131047",
      "occurred_at": "2026-07-23T14:51:40.201Z"
    },
    "from": { "phone_number": "+13124495569" },
    "metadata": null,
    "tags": null,
    "to": { "phone_number": "+14155550100" },
    "whatsapp_id": "wam_01ky7qbvswf3fvyaw3az90391c",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
  },
  "timestamp": "2026-07-23T14:51:40.201Z",
  "type": "whatsapp.failed"
}
Marking an inbound message as read records whatsapp.read in the message's timeline but does not emit a webhook.

Incoming messages

whatsapp.received carries the message's content on top of the fields every message event carries, so an endpoint can act on an inbound message without reading it back. A tap on an interactive message arrives as interactive_reply, and in_reply_to_message_id names the message it answers:
Contoh kode
{
  "data": {
    "direction": "inbound",
    "from": {
      "display_name": "Alex Rivera",
      "phone_number": "+14155550100",
      "username": "alexr"
    },
    "in_reply_to_message_id": "wam_01ky7qbvswf3fvyaw3az90391c",
    "interactive_reply": {
      "list": {
        "description": "Next day to 2 days",
        "slug": "priority_express",
        "text": "Priority Mail Express"
      },
      "type": "list"
    },
    "metadata": null,
    "tags": null,
    "to": { "phone_number": "+13124495569" },
    "whatsapp_id": "wam_01ky8b3xq4gd7pmzn2ka51f7te",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
  },
  "timestamp": "2026-07-23T14:52:04.118Z",
  "type": "whatsapp.received"
}
The other content arms follow the same one-of-these shape: text, image, video, audio, sticker, document, location, contact_cards, and unsupported for a kind the API does not model. GET /v1/whatsapp/messages/{message_id} documents each one.

Next steps