Skip to main content

Media

There are two ways to send a file, and the choice matters a lot for performance.

Meta downloads the file from your server on every send.

curl -X POST https://api.joinotify.com/v1/106540352242922/messages \n -H 'Authorization: Bearer sk_live_xxx' \n -H 'Content-Type: application/json' \n -d '{
"messaging_product": "whatsapp",
"to": "5541987111527",
"type": "image",
"image": { "link": "https://exemplo.com/catalogo.png" }
}'

Good for a one-off file generated on demand — an invoice, a receipt. The URL has to be public, with the right Content-Type and no redirect to a login page.

By media id​

You upload the file once and reuse the id.

1. Upload
curl -X POST https://api.joinotify.com/v1/106540352242922/media \
-H 'Authorization: Bearer sk_live_xxx' \
-F 'messaging_product=whatsapp' \
-F '[email protected];type=image/png'
{ "id": "1234567890" }
2. Send it as many times as you like
curl -X POST https://api.joinotify.com/v1/106540352242922/messages \n -H 'Authorization: Bearer sk_live_xxx' \n -H 'Content-Type: application/json' \n -d '{
"messaging_product": "whatsapp",
"to": "5541987111527",
"type": "image",
"image": { "id": "1234567890" }
}'

For bulk sending, always use the media id. With link, a thousand sends are a thousand downloads from your server. The file stays on Meta for 30 days.

Upload has its own limit: 60 requests per minute.

Limits per type​

TypeAccepted formatsSize
ImageJPEG, PNG5 MB
VideoMP4, 3GPP — H.264 + AAC16 MB
AudioAAC, AMR, MP3, MP4 Audio, OGG (OPUS only)16 MB
Documentany100 MB
StickerWebP100 KB static · 500 KB animated

A video with another codec is accepted at upload and fails at delivery, with no clear error. If a video "disappears", check the codec before anything else.

Receiving media​

When the customer sends a file, the webhook carries only the id — not the content:

{
"messages": [
{
"from": "5541999999999",
"id": "wamid.HBgN...",
"type": "image",
"image": { "id": "9876543210", "mime_type": "image/jpeg", "sha256": "..." }
}
]
}

Downloading takes two steps.

1. Get the temporary URL
curl https://api.joinotify.com/v1/9876543210 \
-H 'Authorization: Bearer sk_live_xxx'
{
"url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...",
"mime_type": "image/jpeg",
"file_size": 184320,
"id": "9876543210"
}
2. Download — the Authorization header is required
curl -L 'https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...' \
-H 'Authorization: Bearer sk_live_xxx' \
-o received.jpg
That URL is not public

Opening the link in a browser returns an error, and putting it in an <img src> does not work. It only responds with the Authorization header.

Don't store that URL in your database as if it were permanent: it expires. Download the file, save it to your own storage and keep your URL.

async function downloadMedia(mediaId) {
const headers = { Authorization: `Bearer ${process.env.JOINOTIFY_TOKEN}` };

const meta = await fetch(`https://api.joinotify.com/v1/${mediaId}`, { headers });
const { url, mime_type } = await meta.json();

const file = await fetch(url, { headers });
const buffer = Buffer.from(await file.arrayBuffer());

// Store it yourself and return YOUR url.
return storage.save(buffer, { mimeType: mime_type });
}

Do this asynchronously, outside the webhook handler — your endpoint has to answer 2xx in a few seconds or the delivery counts as failed and gets retried.

Deleting​

curl -X DELETE https://api.joinotify.com/v1/1234567890 \
-H 'Authorization: Bearer sk_live_xxx'

Messages already delivered with that file keep working for the customer. Deleting only frees the storage and prevents new sends with that id.