Media
Sending
Section titled “Sending”Media is a third kind of send, alongside text and template, and follows the same
window rule as text — there is no media template to fall back on.
curl -X POST "https://vaarto.com/api/v1/messages/whatsapp" \ -H "Authorization: Bearer $VAARTO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+919876543210", "media": { "type": "image", "link": "https://yourcdn.com/invoice-1029.png", "caption": "Your invoice" } }'const res = await fetch("https://vaarto.com/api/v1/messages/whatsapp", { method: "POST", headers: { Authorization: `Bearer ${process.env.VAARTO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ to: "+919876543210", media: { type: "image", link: "https://yourcdn.com/invoice-1029.png", caption: "Your invoice", }, }),});
const { data } = await res.json();import os, requests
res = requests.post( "https://vaarto.com/api/v1/messages/whatsapp", headers={"Authorization": f"Bearer {os.environ['VAARTO_API_KEY']}"}, json={ "to": "+919876543210", "media": { "type": "image", "link": "https://yourcdn.com/invoice-1029.png", "caption": "Your invoice", }, }, timeout=10,)
data = res.json()["data"]$data = Http::withToken(env('VAARTO_API_KEY')) ->post('https://vaarto.com/api/v1/messages/whatsapp', [ 'to' => '+919876543210', 'media' => [ 'type' => 'image', 'link' => 'https://yourcdn.com/invoice-1029.png', 'caption' => 'Your invoice', ], ]) ->json('data');| Field | Notes |
|---|---|
type |
image, video, audio, document or sticker |
link |
a public HTTPS URL. Meta fetches it — the bytes never pass through this API |
id |
media already uploaded to Meta. Give link or id, never both |
caption |
shown under the attachment. Not accepted on audio or sticker |
filename |
what the recipient’s phone saves it as. Documents only |
Reading one back
Section titled “Reading one back”An inbound attachment arrives as a media id, not a URL, and the URL Meta exchanges it for expires in minutes and needs the account’s token. So the bytes are streamed through this API instead:
curl -L "https://vaarto.com/api/v1/messages/whatsapp/MESSAGE_ID/media" \ -H "Authorization: Bearer $VAARTO_API_KEY" -o attachment.binconst res = await fetch("https://vaarto.com/api/v1/messages/whatsapp/MESSAGE_ID/media", { headers: { Authorization: `Bearer ${process.env.VAARTO_API_KEY}` },});
const bytes = Buffer.from(await res.arrayBuffer());import os, requests
res = requests.get( "https://vaarto.com/api/v1/messages/whatsapp/MESSAGE_ID/media", headers={"Authorization": f"Bearer {os.environ['VAARTO_API_KEY']}"}, timeout=10,)
open("attachment.bin", "wb").write(res.content)$response = Http::withToken(env('VAARTO_API_KEY')) ->get('https://vaarto.com/api/v1/messages/whatsapp/MESSAGE_ID/media');
file_put_contents('attachment.bin', $response->body());Scope MEDIA_READ, not MESSAGES_READ — the log is text and status, this is the file
itself.
It is also the one endpoint that does not answer in the { data } envelope: the response
is the file, with content-type from Meta and a content-disposition filename for
documents. GET /messages/whatsapp/:id tells you in advance whether there is anything to
fetch, as hasMedia.
Attachments do not expire here. Meta keeps media for about 30 days and then stops serving it, so every attachment is copied into our own storage shortly after it arrives and this endpoint reads it back from there. A thread from last year opens its files exactly like a thread from this morning.
A 410 is therefore now rare, and means the attachment predates archiving or was already gone at Meta before it could be copied.
This covers attachments you sent as a link too. Meta fetches that URL itself, so
nothing passed through this API at the time — the link is archived separately, and
once it is, the attachment reads back like any other. Until then hasMedia is
false and this endpoint answers 409 MEDIA_NOT_ARCHIVED. The URL must still be
reachable when the copy is taken, so do not take it down the moment Meta has
fetched it.
| Failure | HTTP | Code |
|---|---|---|
| Never archived, and Meta’s 30 days have run out | 410 | MEDIA_EXPIRED |
| Not yet archived and Meta would not serve it | 410 | MEDIA_UNAVAILABLE |
Sent as a link and not archived yet |
409 | MEDIA_NOT_ARCHIVED |
| Meta could not be reached, or refused | 502 | MEDIA_UNAVAILABLE |