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:
message.receivedannounces the attachment with"state": "processing"and no id yet — we are still fetching it from WhatsApp and scanning itmedia.availablearrives with themedia_idonce 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.