# Receiving WhatsApp images

A photo or graphic a contact sends arrives as an inbound message carrying `image`: a reference to the file Bird stored for you, plus whatever caption they typed under it.

## What an inbound image carries

```json
{
  "id": "wam_01kya19eknftrs2s6p82asmvnh",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "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?"
  },
  "created_at": "2026-08-25T09:04:11Z"
}
```

| Field       | What it carries                                                        |
| ----------- | ---------------------------------------------------------------------- |
| `id`        | The stored file, to pass as `media_id` when fetching the bytes         |
| `url`       | A Bird URL, fetched with your API key                                  |
| `mime_type` | The media type WhatsApp reported for the file, such as `image/jpeg`    |
| `caption`   | The text the contact typed under the image; absent when they sent none |

`id` and `mime_type` appear only on an inbound image: Bird learns both by fetching the file, and never stored the one you sent. An outbound image reads back with the `url` you supplied and no `id`.

Treat `mime_type` as WhatsApp's report rather than a guarantee, and branch on it instead of on the file extension in `url`, which carries none.

## Fetching the bytes

`url` and `id` both point at the same stored file: pass the message ID and the media `id` to the channel's media method, in any SDK, the CLI, or cURL. The hub's [fetching inbound media](/docs/guides/whatsapp/receiving-whatsapp#fetching-inbound-media) carries that call in every language, along with the redirect and header rules it follows.

The message and its media expire together, 30 days after the message arrives; the hub's [fetching inbound media](/docs/guides/whatsapp/receiving-whatsapp#fetching-inbound-media) owns that window and what the reads return once it passes. Store any image you need for longer while the message is still readable.

## The webhook payload

`whatsapp.received` carries the `image` arm on the event envelope, media reference and all:

```json
{
  "type": "whatsapp.received",
  "timestamp": "2026-08-25T09:04:11.118Z",
  "data": {
    "whatsapp_id": "wam_01kya19eknftrs2s6p82asmvnh",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "direction": "inbound",
    "from": { "phone_number": "+14155550100" },
    "to": { "phone_number": "+13124495569" },
    "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?"
    },
    "tags": null,
    "metadata": null
  }
}
```

The webhook fires when the message arrives, which is also when the retention window starts, so an endpoint that queues the fetch rather than doing it inline has the whole window to catch up.

## Things to watch

- **The caption belongs to the contact.** It arrives as plain text with no formatting or entity information, so render it as text.
- **One image per message.** A contact sending several photos produces several inbound messages, each with its own `id` and its own media reference. Group them by `from` and arrival time; no array collects them.

## Next steps

- [How receiving works](/docs/guides/whatsapp/receiving-whatsapp): the inbound envelope, media fetching, and the `whatsapp.received` webhook
- [WhatsApp image messages](/docs/guides/whatsapp/message-types/images): the send side of the same arm
- [Receiving documents](/docs/guides/whatsapp/receiving-whatsapp/documents): the same media shape, with the sender's filename
- [WhatsApp events](/docs/guides/whatsapp/events): the full event list, over the API or webhooks