waiaconnect

Documentation / Essential

Send media (image · document · audio · video)

POST /v1/messages

Markdown

Connect does not store files. To send a file of your own, host it and send its link (link) — Meta downloads it, not us. That is the main path, not the fallback.

Media identifiers (id) are the ones of files you RECEIVED: they arrive inside an inbound message event (data.message.image.id, .document.id, …). There is no upload endpoint — GET /v1/media/:mediaId is download-only — so there is no way to obtain an id for a file of yours.

⚠ And the uncomfortable part, stated as it is: Meta usually lets you reuse an inbound identifier to reply from the same number, but it does not guarantee it and we have not verified it against real Meta yet. We neither claim it nor deny it. If you need it to work every time, use the link.

Need to send ALWAYS the same file — a price list, a how-to, a catalogue? Host it and send the link. That is the whole answer: there is no id of your own to store and reuse. And this is not a limitation being papered over — we don't store files because we don't store content (not your message text either). It is exactly what our privacy policy promises, applied here.

How to send it. POST /v1/messages with type image/document/audio/video and, inside that type's object, link or id — exactly one of the two. caption for image/document/video; audio takes no caption; document may add a filename (the name the recipient sees).

What the link needs for Meta to accept it: be https and point straight at the file (the response must be the bytes — no login, no interstitial page); carry the right Content-Type — if it doesn't match the file, Meta rejects it; and stay within Meta's limits: image JPEG/PNG 5 MB, audio (aac · amr · mp3 · m4a · ogg OPUS only) 16 MB, video mp4/3gpp 16 MB, document (pdf · doc/docx · xls/xlsx · ppt/pptx · txt) 100 MB.

⚠ The most common trap: Google Drive, Dropbox and friends do NOT work. A "share" link returns an HTML interstitial, not the file bytes: Meta downloads that page and rejects the send. Serve it from a URL that answers with the file itself.

curl -X POST https://api.waiaconnect.com/v1/messages \
  -H "Authorization: Bearer wc_live_YOUR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"connectionId":"conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","to":"5493511234567","type":"image","image":{"link":"https://example.com/product.jpg","caption":"New product 📦"}}'
# document: {"type":"document","document":{"link":"https://…/invoice.pdf","filename":"invoice.pdf","caption":"Your invoice"}}
# audio (no caption): {"type":"audio","audio":{"link":"https://…/note.ogg"}}
# video: {"type":"video","video":{"link":"https://…/demo.mp4","caption":"Demo"}}
# by media id instead of a link: {"image":{"id":"1234567890"}}
# ...but the only ids that exist are the ones you RECEIVED (from a message.received
# event); there is no upload endpoint. To send a file of your own, host it and link it.
// Media is sent by { link } (Meta fetches the URL) or { id }. The only media ids that
// exist are the ones you RECEIVED: there is no upload endpoint. Host your own files.
const res = await fetch("https://api.waiaconnect.com/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": "Bearer wc_live_YOUR_API_KEY",
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "connectionId": "conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "to": "5493511234567",
  "type": "image",
  "image": {
    "link": "https://example.com/product.jpg",
    "caption": "New product 📦"
  }
})
});
console.log(await res.json()); // { id: "msg_…", status: "queued" }
<?php
// Media is sent by ["link" => …] (Meta fetches it) or ["id" => …]. The only media ids
// that exist are the ones you RECEIVED: there is no upload endpoint. Host your own files.
$ch = curl_init("https://api.waiaconnect.com/v1/messages");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer wc_live_YOUR_API_KEY",
    "Idempotency-Key: " . bin2hex(random_bytes(16)),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode(["connectionId" => "conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","to" => "5493511234567","type" => "image","image" => ["link" => "https://example.com/product.jpg","caption" => "New product 📦"]]),
]);
echo curl_exec($ch);
import requests, uuid
# Media is sent by {"link": …} (Meta fetches it) or {"id": …}. The only media ids that
# exist are the ones you RECEIVED: there is no upload endpoint. Host your own files.
res = requests.post("https://api.waiaconnect.com/v1/messages",
  headers={
    "Authorization": "Bearer wc_live_YOUR_API_KEY",
    "Idempotency-Key": str(uuid.uuid4()),
  },
  json={"connectionId":"conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","to":"5493511234567","type":"image","image":{"link":"https://example.com/product.jpg","caption":"New product 📦"}})
print(res.json())  # {"id": "msg_…", "status": "queued"}

⚠ An identifier that arrives in a webhook expires after 7 days (the ones Meta's upload API returns last 30). Once expired it is good neither for downloading nor for resending.

Meta caches your link for 10 minutes. If you change the file behind the SAME URL, append a random query string (?v=…) or you'll send the old one.

If the link doesn't work you find out at two moments. (1) On accept, as a 400 with its code: MEDIA_LINK_OR_ID_REQUIRED (you sent neither), MEDIA_LINK_AND_ID (you sent both), MEDIA_LINK_INVALID (not a well-formed https URL), AUDIO_CAPTION_NOT_ALLOWED.

(2) If it passed validation but Meta could not fetch it, the rejection lands AFTER the 202: check GET /v1/messages/:id → lastErrorCode = MEDIA_DOWNLOAD_FAILED (unreachable link or interstitial page), MEDIA_FILE_REJECTED (format or size) or MEDIA_TYPE_UNSUPPORTED. What to do: serve the file directly, with its Content-Type, within the limits above.

To download a file someone sent you, see "Download inbound media".