Media messages: images, documents, audio, and the two-step upload nobody expects
Sending a photo or a PDF isn't one API call the way a text message is — it's an upload step and a separate send step, and receiving media back works in reverse from what most people guess.
3 min read
A text message is one call. A media message is two.
Sending plain text is a single request: a phone number, a message type, a body. Sending an image, document, audio clip, or video adds a step in front of that, because the file itself has to exist somewhere Meta can serve it from before a message can point at it.
Text message: POST /messages { type: "text", text: { body: "..." } }
-> done, one call
Media message: POST /{phone-number-id}/media (upload the file, multipart/form-data)
-> get back a media ID
POST /messages { type: "image", image: { id: "<media-id>" } }
-> done, two calls
The upload call is scoped to the sending phone number, not the WABA — the file gets associated with that number's media store, and the resulting media ID is only usable from sends made by that same number.
Two ways to reference media: an ID, or a link
Besides uploading first, most media types also accept a plain public link instead of a media id — Meta fetches the file from that URL at send time rather than from its own store:
{ "type": "image", "image": { "link": "https://cdn.example.com/receipt-4821.jpg" } }The tradeoff: a link send depends on that URL staying reachable and stable at the moment WhatsApp fetches it, and re-sending the same file to many recipients re-fetches it every time (or relies on Meta's own caching). Uploading once and reusing the resulting id across many sends is the better default for anything sent repeatedly — a receipt template with a logo, a document sent to a batch of recipients.
Type limits worth knowing before a file gets rejected
| Type | Common formats | Size limit |
|---|---|---|
| Image | JPEG, PNG | 5 MB |
| Document | PDF, DOCX, XLSX, and more | 100 MB |
| Audio | AAC, MP3, OGG (opus) | 16 MB |
| Video | MP4, 3GP | 16 MB |
| Sticker | WEBP | 100 KB (static), 500 KB (animated) |
A file that exceeds its type's limit isn't resized or rejected gracefully mid-flow — the upload call itself fails, so this is worth validating client-side before ever hitting Meta's API.
Receiving media runs backwards from sending it
An inbound media message's webhook payload never contains the file itself, only a media ID and a MIME type:
{
"type": "image",
"image": { "id": "<media-id>", "mime_type": "image/jpeg", "sha256": "..." }
}Getting the actual bytes takes two more calls: GET /{media-id} returns a short-lived, signed download URL, and a second request against that URL returns the file itself. Media IDs from inbound webhooks expire in about 7 days — a receiver that wants to keep the file needs to download and store it promptly rather than holding onto the ID for later.
Location messages aren't media at all
A location message looks like it belongs in this category but isn't — there's no upload step, no media ID, just coordinates in the message body itself:
{ "type": "location", "location": { "latitude": 37.4847, "longitude": -122.1477, "name": "Store #12" } }Both sending and receiving a location are single-call, JSON-only operations — worth remembering when reaching for the upload flow out of habit.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the correct sequence to send an image through the Cloud API?
2. A business sends the same logo image as part of a receipt template to thousands of recipients. What's the better approach: a `link`, or an uploaded `id`?
3. An inbound webhook fires for a customer's photo message. What does the payload actually contain?
4. A team stores only the media ID from an inbound webhook and tries to download the file 10 days later. What happens?