Attachments
Send images, video, audio, documents and stickers by uploading bytes or naming a public URL, and download what customers send.
A media part carries one image, video, audio file, document or sticker. On a
send you tell the API where the bytes are in one of two ways; on a read the
part tells you where to fetch them. The three identifiers are separate
namespaces and never overlap:
| Field | Direction | Meaning |
|---|---|---|
upload_ref |
send | Your durable handle from POST /v1/media, reusable in this account and partition. |
source_url |
send | A public https address the API fetches once, before accepting the send. |
media_id |
read only | The channel’s transient inbound identifier. Never accepted on a send. |
url |
read only | The authenticated download address for a retained inbound file. Never accepted on a send. |
A sent part carries exactly one of upload_ref or source_url. Both, or
neither, is refused with HTTP 422 and a JSON Pointer at the offending field.
Option 1: upload, then send the handle
Section titled “Option 1: upload, then send the handle”Post the exact bytes to POST /v1/media with their
media type, then reference the returned upload_ref from the part. The handle
is reusable across sends in the same account and test/live partition; it has
no fixed expiry but is not permanent.
UPLOAD_REF=$(curl -sS -X POST "https://whatsapp.messages.api.linqapp.com/v1/media?kind=image" \ -H "Authorization: Bearer ${LINQ_WHATSAPP_API_KEY}" \ -H "Content-Type: image/png" \ --data-binary @photo.png | jq -r .upload_ref){ "to": "+15551234567", "parts": [{ "type": "media", "kind": "image", "upload_ref": "upload.0123456789abcdef0123456789abcdef" }]}Use this route for large files, for bytes you will send more than once, and whenever the file is not already reachable on the public internet.
Option 2: name a public URL
Section titled “Option 2: name a public URL”If the file is already served from a public https host, put its address in
source_url and skip the upload. The API fetches the address exactly once,
before it accepts the send, and stores the exact bytes as an upload in your
account. The accepted message — and every later transcript read and webhook —
carries that upload’s upload_ref, never the address. No retry fetches the
address again, and the address is never forwarded to the channel.
{ "to": "+15551234567", "parts": [{ "type": "media", "kind": "image", "source_url": "https://cdn.example.com/menu.jpg" }]}The response’s part shows what was stored:
{ "id": "msg_158ff7124ea56816e5006438177a773c", "status": "accepted", "parts": [{ "type": "media", "kind": "image", "upload_ref": "upload.4293e657b84aa6a0c561219cdee74b71" }]}Send with an Idempotency-Key: a replay of the same request under the same
key returns the same message and the same upload_ref without fetching again,
which is what makes a timed-out send safe to retry.
What the address must satisfy
Section titled “What the address must satisfy”- Absolute
httpson the default port, with no credentials and no fragment. - Resolves to a public destination — on every redirect hop, at most three. Private, loopback, link-local, multicast and cloud-metadata destinations are refused, whatever name resolves to them.
- Answers
200with the complete bytes, their exactContent-Type, and noContent-Encoding, within the fetch deadline. The API never sniffs a media type from bytes and never decodes a compressed answer. - The bytes satisfy the same media-type allowlist and size ceiling that
POST /v1/mediaapplies to the part’skind. Nothing is converted or truncated.
Refusals
Section titled “Refusals”Every refusal is HTTP 422 with error.param at /parts/0/source_url,
except capacity, which is a 503 you may retry shortly.
| Code | Meaning |
|---|---|
5007 media_source_invalid |
The address failed the policy: scheme, port, credentials, a non-public destination or too many redirects. Fix the address. |
5008 media_source_unavailable |
The host was unreachable, timed out, answered something other than 200, or delivered fewer bytes than promised. |
5009 media_source_unsupported |
The bytes do not fit the kind: wrong media type, over the ceiling, an unreadable sticker, or an encoded or untyped body. |
5010 media_source_capacity_exhausted |
No fetch capacity was free. Retry shortly, or upload the bytes instead. |
For large or slowly served files, prefer the upload route: the fetch runs inside the send request and has a bounded deadline.
Send a voice note
Section titled “Send a voice note”Set "voice": true on an outbound audio part to send it as a voice note,
shown with a waveform and a play button. false, or leaving it out, sends an
ordinary audio file.
curl -sS -X POST "https://whatsapp.messages.api.linqapp.com/v1/chats/${CHAT_ID}/messages" \ -H "Authorization: Bearer ${LINQ_WHATSAPP_API_KEY}" \ -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ -H "Content-Type: application/json" \ -d '{"parts":[{"type":"media","kind":"audio","upload_ref":"upload.0123456789abcdef0123456789abcdef","voice":true}]}'- The audio must be mono Opus in an OGG container, stored as
audio/ogg: upload it toPOST /v1/mediawithkind=audioandContent-Type: audio/ogg, or name asource_urlthat servesaudio/ogg. - The bytes are sent exactly as supplied — never converted or inspected — so encode them as mono Opus before uploading.
- A voice note of 512 KB or less shows a play button. A larger one still arrives as a voice note, but the recipient must download it before it plays. The ordinary audio size ceiling still applies.
- Voice notes are supported on sends to individual chats.
voice: true on an upload that is not stored as audio/ogg, voice on any
kind other than audio, and "voice": null are all refused with HTTP 422,
error.code 1004 invalid_content,
and a JSON Pointer at the field, such as /parts/0/voice.
Kinds, types and ceilings
Section titled “Kinds, types and ceilings”kind selects the rules. Captions apply to image, video and document; a
filename applies to documents only. The allowed media types and byte ceilings
per kind are the upload operation’s, published in its
x-media-upload-profiles extension in the
API reference.
Download incoming attachments
Section titled “Download incoming attachments”An incoming media part carries a response-only url that requires your
account’s Bearer key. Its response-only availability says whether the
download answers the bytes: available, or unavailable with an
unavailable_reason (timed_out, capture_failed or expired today). A
message.received event can be recorded once its attachments are captured or
have failed, or ten minutes after the message arrived, so the file can be ready
when the event arrives. This is being enabled gradually; a part with no
availability may still be capturing.
The full procedure — including what each value means and the pending, expired and not-found answers — is in Sending messages → Download incoming attachments.
Related
Section titled “Related”- Sending messages — routes, the customer window and idempotency
- Errors — the envelope and the code ranges
- API reference: media and attachments