Sending and Receiving Media

Upload images, audio, video and documents, send them to chats and groups, and download inbound media before the retention window closes.

Updated mediamessagesstorage

Media in Wapito is one field and one store. The field is media on every send endpoint that carries a file, and it accepts three forms: a public URL, a data URI, or the id of a file you uploaded earlier. The store is where those uploads live, and where every inbound file is copied to the moment it arrives, so a photo a customer sent this morning is still there long after WhatsApp itself has discarded it. This page covers both directions, the size and type limits, and the signed links that let a browser fetch a file without ever seeing your channel token.

Sending a file

Seven endpoints send media, one per bubble type WhatsApp renders:

EndpointWhat the recipient sees
POST /messages/imageA photo with an optional caption. JPEG, PNG or WebP.
POST /messages/videoA video with an optional caption.
POST /messages/documentA file bubble with the name you give it. Bytes are never re-encoded.
POST /messages/stickerA sticker. WebP only; no caption.
POST /messages/audioAn audio file with a play bar.
POST /messages/voiceA voice note: the waveform bubble, with a played receipt. Wapito converts MP3, WAV and M4A to the Opus-in-OGG format WhatsApp needs.
POST /messages/shortA round video note.

They share one request body: to, media, and depending on the type caption and filename.

curl -X POST https://api.wapito.com/v1/messages/document \
  -H "Authorization: Bearer $WAPITO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15551234567",
    "media": "https://acme.example/invoices/4182.pdf",
    "filename": "Invoice-4182.pdf",
    "caption": "Invoice for order #4182"
  }'

The three forms of media

A URL. https:// only, and the host must resolve to a public address. Wapito probes it with a HEAD request — that is where the size and the type are read from — and the protocol engine then downloads it, so the URL is never shown to the recipient and never has to be reachable from WhatsApp itself. Up to two redirects are followed, each one checked again. A URL that points at a private range, a plain http URL, or one that does not answer within ten seconds is refused with 400 invalid_request and a details.reason saying why.

A data URI. data:application/pdf;base64,JVBERi0… — the bytes inline. Convenient for small files generated on the fly; for anything large, upload first.

A media id. med_01JRQ8F4X9N2K7YB3C5V6W8H0T, returned by an upload or found on any media message. The file is already in the store, so a send is immediate and the same id can go to a hundred recipients without a hundred fetches. This is the right form for anything you send more than once.

When the type or the name cannot be inferred — an upload endpoint that serves every file as application/octet-stream, say — pass an object instead of a string:

{ "to": "+15551234567", "media": { "url": "https://acme.example/dl?id=4182", "mimetype": "application/pdf", "filename": "Invoice-4182.pdf" } }

Uploading first

POST /media takes a multipart form with a file part and an optional filename, and returns the stored file:

curl -X POST https://api.wapito.com/v1/media \
  -H "Authorization: Bearer $WAPITO_TOKEN" \
  -F "file=@./Invoice-4182.pdf" \
  -F "filename=Invoice-4182.pdf"
{
  "id": "med_01JRQ8F4X9N2K7YB3C5V6W8H0T",
  "channel_id": "ch_01JRQ8F4X9N2K7YB3C5V6W8H0T",
  "filename": "Invoice-4182.pdf",
  "mimetype": "application/pdf",
  "size_bytes": 48210,
  "link": "https://api.wapito.com/v1/media/med_01JRQ8F4X9N2K7YB3C5V6W8H0T?exp=1790064121&sig=8f2c1d94b6a70e35",
  "created_at": "2026-09-15T07:22:01.000Z",
  "expires_at": "2026-09-22T07:22:01.000Z"
}

Uploads expire like inbound files do (below), so an upload is a staging area for the sends that follow, not an archive. DELETE /media/{id} removes one early.

Limits

Two checks run before any WhatsApp traffic happens, so a file that is too big or the wrong type costs the number nothing:

  • Size. 16 MB on Sandbox and 64 MB on Premium — the decoded bytes of a data URI, the multipart body of an upload, or the Content-Length a URL reports. Over the cap answers 413 payload_too_large. The cap is also in GET /channel/limits as media_max_bytes.
  • Type. The store keeps an allow-list, not a deny-list, of media types: the common image formats (JPEG, PNG, GIF, BMP, TIFF, HEIC), WebP for stickers, MP4, 3GPP, QuickTime, WebM and Matroska video, MP3, AAC, OGG, Opus, WAV, AMR and WebM audio, and PDF, Office, OpenDocument, RTF and ZIP documents. Anything else — SVG above all, which can carry script — answers 415 unsupported_media_type. The type comes from the URL's Content-Type header, the data URI's declared type or the mimetype you pass explicitly — never from the file extension. A server that answers without a usable Content-Type gets 400 invalid_request with details.reason: "media_mimetype_required" until you pass one.

Media sends count toward the daily sent quota exactly like text, one per message.

Receiving a file

An inbound photo, document or voice note arrives as a messages event whose message object has type set and one typed field beside it:

{
  "id": "false_15551234567@s.whatsapp.net_9F31A0C4D7E2B6081A55",
  "chat_id": "15551234567@s.whatsapp.net",
  "from": "15551234567",
  "from_me": false,
  "type": "image",
  "timestamp": 1789459200000,
  "image": {
    "id": "med_01JRQ8F4X9N2K7YB3C5V6W8H0T",
    "link": "https://api.wapito.com/v1/media/med_01JRQ8F4X9N2K7YB3C5V6W8H0T?exp=1790064121&sig=8f2c1d94b6a70e35",
    "mime_type": "image/jpeg",
    "file_size": 184213,
    "file_name": null,
    "caption": "Here is the receipt"
  }
}

By the time the event reaches you the file has been copied out of the protocol engine into the Wapito store, and link points at that copy. The engine itself keeps a file for about an hour; the copy is what makes the link usable for days. If the copy failed — a file WhatsApp had already dropped, or one that could not be decrypted — the event still ships, with { "id": null, "error": "unavailable" } in place of the media block, so a missing file never delays the message that carried it.

Your webhook handler should return 200 first and fetch the file from a worker: media downloads are the slowest thing a handler can do inline, and the ten-second delivery timeout is for the acknowledgement, not for the download.

A link is a URL to GET /media/{id} with two query parameters, exp and sig. The signature covers the media id and the expiry, so neither can be edited to reach another file or extend the window, and the link needs no Bearer header — that is what makes it safe to hand to a browser, an e-mail, or a downstream service that must never hold the channel token. The same endpoint also accepts the channel token, without exp and sig, for your own back end.

PlanInbound and uploaded files are kept for
Sandbox24 hours
Premium7 days

That window is the file's expires_at, and it is also how long its signed links work. After it the file is deleted and the link answers 404 not_found. Two consequences:

  • If you need the file for longer, fetch it and store your own copy. The store is a hand-off, not an archive, and the security page lists this retention alongside everything else Wapito keeps.
  • A link you saved yesterday may have expired even though the file has not, if you are close to the window. GET /messages/{id}/media returns the file's current record with a freshly signed link.
# Any HTTP client can follow a signed link — no Authorization header.
curl -o receipt.jpg \
  "https://api.wapito.com/v1/media/med_01JRQ8F4X9N2K7YB3C5V6W8H0T?exp=1790064121&sig=8f2c1d94b6a70e35"

Try it on your own number

Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.