# 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](/docs/guides/whatsapp/message-log) 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](/docs/knowledge-base/whatsapp/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:

```json
{
  "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:

| Field                                                       | What an inbound one carries                                             |
| ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| [`text`](/docs/guides/whatsapp/message-types/plain-text)    | `body`, the message the contact typed                                   |
| [`image`](/docs/guides/whatsapp/message-types/images)       | An `id`, `url`, `mime_type`, and any `caption`                          |
| [`video`](/docs/guides/whatsapp/message-types/video)        | The same media fields, plus any `caption`                               |
| [`audio`](/docs/guides/whatsapp/message-types/audio)        | The same media fields; no caption exists on this arm                    |
| [`sticker`](/docs/guides/whatsapp/message-types/stickers)   | The same media fields, plus `animated`                                  |
| [`document`](/docs/guides/whatsapp/message-types/documents) | The same media fields, plus any `filename` and `caption`                |
| [`location`](/docs/guides/whatsapp/message-types/location)  | `latitude` and `longitude`, and sometimes `name`, `address`, or a `url` |
| `interactive_reply`                                         | The `slug` and `text` of the button or row the contact tapped           |
| [`contact_cards`](#contact-cards)                           | One 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:

```json
{
  "audio": {
    "id": "waf_01kyb2m4xq7whs0d8n3prv6tez",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez",
    "mime_type": "audio/ogg; codecs=opus",
    "voice": true
  }
}
```

```json
{
  "video": {
    "id": "waf_01kyc4n5yr8xit1e9o4qsw7ufa",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyc4n5yr8xit1e9o4qsw7ufa",
    "mime_type": "video/mp4",
    "caption": "The rattle starts around 0:12"
  }
}
```

```json
{
  "document": {
    "id": "waf_01kyd5p6zs9yju2f0p5rtx8vgb",
    "url": "https://platform.bird.com/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyd5p6zs9yju2f0p5rtx8vgb",
    "mime_type": "application/pdf",
    "filename": "invoice-A1B2C3.pdf"
  }
}
```

`id` and `mime_type` appear only on an inbound message, since Bird learns them by fetching the file. [Fetching inbound media](#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:

```json
{
  "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`:

```json
{
  "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](/docs/guides/whatsapp/message-types/interactive/location-requests) comes back as an ordinary inbound `location`:

```json
{
  "location": {
    "latitude": 37.7793,
    "longitude": -122.4193,
    "name": "Embarcadero Plaza",
    "address": "1 Market St, San Francisco, CA 94105"
  }
}
```

A [contact info request](/docs/guides/whatsapp/message-types/interactive/contact-info-requests) 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.

```json
{
  "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:

```json
{
  "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:

<!-- bird:tabs typescript,python,go,php,cli,curl -->

```typescript
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());
```

```python
redirect = client.get(
    "/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez"
)
bytes_ = httpx.get(redirect["url"]).content
```

```go
var out struct {
	URL string `json:"url"`
}
if err := client.Get(context.Background(), "/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez", &out); err != nil {
	log.Fatal(err)
}
resp, err := http.Get(out.URL)
```

```php
$redirect = $bird->get('/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez');
$bytes = file_get_contents($redirect['url']);
```

```cli
bird whatsapp media wam_01kya19eknftrs2s6p82asmvnh waf_01kyb2m4xq7whs0d8n3prv6tez --output part.jpg
```

```curl
curl -L "https://us1.platform.bird.com/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez" \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -o part.jpg
```

<!-- /bird:tabs -->

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:

```json
{
  "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](/docs/guides/whatsapp/sending-whatsapp#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:

```json
{
  "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](/docs/guides/whatsapp/events#webhooks) for the envelope in full and the rest of the event list.

## Next steps

- [Service messages](/docs/guides/whatsapp/message-types): the read shape of each of the eight content arms
- [How sending works](/docs/guides/whatsapp/sending-whatsapp): replying inside the service window, and quoting a message
- [WhatsApp events](/docs/guides/whatsapp/events): the full event list, over the API or webhooks
- [WhatsApp log](/docs/guides/whatsapp/message-log): browsing the conversation in the dashboard