Documentation
Sign inGet started

Receiving WhatsApp messages

Inbound messages land on the same resource as outbound ones, so there is no separate inbox endpoint to poll. Read them with the message list in the dashboard, or over the API with GET /v1/whatsapp/messages/{id} after filtering the list to inbound.
Every inbound message resets the customer service window to a fresh 24 hours, which is what makes a free-form reply of your own deliverable.

What an inbound message carries

Every inbound message shares one envelope: an id, direction: "inbound", the contact in from, your own number in to, a status of received, and a created_at. Exactly one content field sits alongside it, naming what the contact sent:
Codevoorbeeld
{
  "id": "wam_01kya19eknftrs2s6p82asmvnh",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "text": { "body": "Is my order out for delivery yet?" },
  "created_at": "2026-08-25T09:04:11Z"
}
One of these content fields carries what they sent:
FieldWhat an inbound one carries
textbody, the message the contact typed
imageAn id, url, mime_type, and any caption
videoThe same media fields, plus any caption
audioThe same media fields; no caption exists on this arm
stickerThe same media fields, plus animated
documentThe same media fields, plus any filename and caption
locationlatitude and longitude, and sometimes name, address, or a url
interactive_replyThe slug and text of the button or row the contact tapped
contact_cardsOne or more shared contact cards. Inbound only
unsupported takes their place for a message type the API does not model.
An audio, video or document arrives the same way an image does, as a reference plus whatever metadata the arm carries:
Codevoorbeeld
{
  "id": "wam_01kyb3q8ndvt6r2h5k9wxc4pfa",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100", "bsuid": "US.13491208655302741918" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "audio": {
    "id": "waf_01kyb2m4xq7whs0d8n3prv6tez",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kyb3q8ndvt6r2h5k9wxc4pfa/media/waf_01kyb2m4xq7whs0d8n3prv6tez",
    "mime_type": "audio/ogg; codecs=opus",
    "voice": true
  },
  "created_at": "2026-08-25T09:07:32Z"
}
Codevoorbeeld
{
  "id": "wam_01kyc5r9pewu7s3j6m0xyd5qgb",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100", "bsuid": "US.13491208655302741918" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "video": {
    "id": "waf_01kyc4n5yr8xit1e9o4qsw7ufa",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kyc5r9pewu7s3j6m0xyd5qgb/media/waf_01kyc4n5yr8xit1e9o4qsw7ufa",
    "mime_type": "video/mp4",
    "caption": "The rattle starts around 0:12"
  },
  "created_at": "2026-08-25T09:11:48Z"
}
Codevoorbeeld
{
  "id": "wam_01kyd6s0qfxv8t4k7n1yze6rhc",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100", "bsuid": "US.13491208655302741918" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "document": {
    "id": "waf_01kyd5p6zs9yju2f0p5rtx8vgb",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kyd6s0qfxv8t4k7n1yze6rhc/media/waf_01kyd5p6zs9yju2f0p5rtx8vgb",
    "mime_type": "application/pdf",
    "filename": "invoice-A1B2C3.pdf"
  },
  "created_at": "2026-08-25T09:15:02Z"
}
id and mime_type appear only on an inbound message, since Bird learns them by fetching the file. Fetching inbound media covers reading the bytes.

Reading quick replies

A tap on a reply button or a list row arrives as its own inbound message carrying interactive_reply. type says which, and the nested object of the same name carries the slug and text you set on the send:
Codevoorbeeld
{
  "id": "wam_01kyb2m4xq7whs0d8n3prv6tez",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "interactive_reply": {
    "type": "button",
    "button": { "slug": "cancel-booking", "text": "Cancel" }
  },
  "created_at": "2026-08-25T09:04:11Z"
}
A list row replaces button with list, which adds the row's description:
Codevoorbeeld
{
  "interactive_reply": {
    "type": "list",
    "list": {
      "slug": "priority_express",
      "text": "Priority Mail Express",
      "description": "Next day to 2 days"
    }
  }
}
The other two interactive types answer without an interactive_reply at all. A location request comes back as an ordinary inbound location:
Codevoorbeeld
{
  "location": {
    "latitude": 37.7793,
    "longitude": -122.4193,
    "name": "Embarcadero Plaza",
    "address": "1 Market St, San Francisco, CA 94105"
  }
}
A contact info request comes back as contact_cards, below. An integration watching only interactive_reply misses both.

Contact cards

contact_cards is the one arm that exists on the read side only: a contact can share a card, but you cannot send one. origin says which way it arrived, contact_request for a button you sent asking for their number and other for a card they shared unprompted. More values can appear later, so treat one you don't recognize as another way of sharing rather than an error.
Codevoorbeeld
{
  "id": "wam_01kyb2m4xq7whs0d8n3prv6tez",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "contact_cards": [
    {
      "origin": "contact_request",
      "phone_numbers": [{ "phone_number": "+14155550100", "type": "CELL" }]
    }
  ],
  "created_at": "2026-08-25T09:04:11Z"
}
Nothing on a card is required. WhatsApp sends the parts the card holds and omits the rest, so a card carrying only an origin still arrives rather than being dropped. A button tap carries the number in phone_numbers and no vcard; a card shared in the chat carries the vcard and usually more besides: name, org, birthday, emails, urls, addresses.
birthday comes off the contact's own device unvalidated, so it's passed through as text in YYYY-MM-DD shape rather than typed as a date. Don't assume it parses.

Fetching inbound media

An inbound image, video, audio, sticker or document arrives as a reference, not the file itself:
Codevoorbeeld
{
  "image": {
    "id": "waf_01kyb2m4xq7whs0d8n3prv6tez",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez",
    "mime_type": "image/jpeg",
    "caption": "Is this the right part?"
  }
}
Fetch the bytes from that url, following the redirect. No SDK carries a typed method for this operation, so the SDK tabs use each client's raw-request escape hatch:
const redirect = await bird.request<{ url: string }>({
  method: "GET",
  path: "/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez",
});
const bytes = await fetch(redirect.url).then((r) => r.arrayBuffer());
It answers 302 to a presigned URL valid for about 15 minutes, 410 once the 30-day retention window lapses, and 404 for media Bird doesn't recognize. The presigned URL carries its own credential, so the redirected hop must not also carry your Authorization header; sending both fails the request. curl drops the header on a cross-host redirect on its own, but a client that forwards headers verbatim needs the Location fetched as a separate, unauthenticated call.

Quoted replies

in_reply_to_message_id names the message an inbound one answers, when WhatsApp marks it as a reply:
Codevoorbeeld
{
  "id": "wam_01kyb2m4xq7whs0d8n3prv6tez",
  "direction": "inbound",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "text": { "body": "Yes, that one" }
}
WhatsApp does not mark every reply, and an unmarked one carries no ID at all. If correlation has to be reliable, put your own reference in metadata on the send instead. See Quoting a message for the outbound side.

The webhook

Subscribe to whatsapp.received to act on an inbound message as it arrives rather than polling the list. The payload carries the content on top of the event envelope, so an endpoint needs no follow-up read:
Codevoorbeeld
{
  "type": "whatsapp.received",
  "timestamp": "2026-08-25T09:04:11.118Z",
  "data": {
    "whatsapp_id": "wam_01kyb2m4xq7whs0d8n3prv6tez",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "direction": "inbound",
    "from": { "phone_number": "+14155550100", "display_name": "Alex Rivera" },
    "to": { "phone_number": "+13124495569" },
    "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
    "interactive_reply": {
      "type": "button",
      "button": { "slug": "cancel-booking", "text": "Cancel" }
    },
    "tags": null,
    "metadata": null
  }
}
See WhatsApp events for the envelope in full and the rest of the event list.

Next steps