API reference

Media

Download attachments your customers send you, and upload files to send.

Media

Attachments move through the API in two directions: files you upload to send, and files your customers send you, which you download.

Downloading an inbound attachment

When someone sends your number a photo, document, video or voice note, you learn about it in two steps:

  1. message.received announces the attachment with "state": "processing" and no id yet — we are still fetching it from WhatsApp and scanning it
  2. media.available arrives with the media_id once it is scanned and stored

The id you need is data.media.id from that event — not the event's own top-level id, and not data.message.id:

{
  "id": "6a5f5728-…",                    ← the event id. Not this one.
  "type": "media.available",
  "data": {
    "media":   { "id": "a3771707-…" },   ← this one
    "message": { "id": "5f2ebda3-…" }    ← the message it came with
  }
}

Then fetch the bytes:

curl -L -o invoice.pdf \
  -H 'Authorization: Bearer cap_a1b2c3d4e5f6.xxxx…' \
  https://your-domain.com/api/v1/media/15bde241-97b6-4a35-af22-8b7bf2a5974a/download

The response streams the file with its original Content-Type and a Content-Disposition carrying the original filename. Large attachments are streamed rather than buffered, so a 60 MB video does not need 60 MB of memory on either side.

Requires the media:read scope.

Check before you fetch

GET /api/v1/media/{id}
{
  "data": {
    "id": "15bde241-97b6-4a35-af22-8b7bf2a5974a",
    "mime_type": "image/jpeg",
    "filename": "image",
    "byte_size": 42476,
    "state": "retained",
    "scanned_at": "2026-07-20T13:06:37+00:00",
    "retention_expires_at": "2026-08-19T13:06:37+00:00",
    "deleted_at": null,
    "created_at": "2026-07-20T13:06:37+00:00"
  }
}

This works in any state, deliberately. An attachment you saw as processing can be polled here — a 404 would read as "wrong id" rather than "not yet".

state Meaning
staged Received, not yet scanned
scanning Being scanned right now
retained Scanned, stored, downloadable
rejected Failed the malware scan; the bytes are gone
expired An upload that was never confirmed inside its staging window
deleted Removed, including after its retention window passed

Only retained can be downloaded. An unknown id returns 404; a known id in any other state returns 422:

{ "message": "The media object is not available for download." }

Prefer waiting for media.available over polling — the event fires the moment the object becomes retained, and polling a large video adds load without arriving sooner.

When no media.available arrives

An attachment that fails the malware scan, or exceeds your plan's size limit, produces no event. Absence is the signal; there is no rejection event to wait for.

A rejected object does still exist, and GET /api/v1/media/{id} returns it with "state": "rejected" — but its id is never announced to you, because message.received carries no ids. Reconcile against the message rather than waiting on an event that will not come.

Retention

Media is kept according to your plan's retention window, visible as retention_expires_at on every object. After it passes, the object is deleted and download returns 422.

Download what you need to keep, when you get it. These are your customers' photos and documents; we hold them to move them, not to archive them for you.

Uploading

See Messages for POST /api/v1/media and how to attach a media_id to an outbound message. Uploads require messages:write, not media:read — the two are separate so a key that only sends can never read your customers' inbound files back out.

Was this guide useful? Report an issue