Media
There are two ways to send a file, and the choice matters a lot for performance.
By link
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.
curl -X POST https://api.joinotify.com/v1/106540352242922/media \
-H 'Authorization: Bearer sk_live_xxx' \
-F 'messaging_product=whatsapp' \
{ "id": "1234567890" }
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
| Type | Accepted formats | Size |
|---|---|---|
| Image | JPEG, PNG | 5 MB |
| Video | MP4, 3GPP — H.264 + AAC | 16 MB |
| Audio | AAC, AMR, MP3, MP4 Audio, OGG (OPUS only) | 16 MB |
| Document | any | 100 MB |
| Sticker | WebP | 100 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.
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"
}
curl -L 'https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...' \
-H 'Authorization: Bearer sk_live_xxx' \
-o received.jpg
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.
Recommended flow on receipt
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.