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.

Beginner

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.

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

TypeCommon formatsSize limit
ImageJPEG, PNG5 MB
DocumentPDF, DOCX, XLSX, and more100 MB
AudioAAC, MP3, OGG (opus)16 MB
VideoMP4, 3GP16 MB
StickerWEBP100 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?