Skip to content
Linq Copy agent prompt
Messaging

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.

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.

Terminal window
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.

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.

  • Absolute https on 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 200 with the complete bytes, their exact Content-Type, and no Content-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/media applies to the part’s kind. Nothing is converted or truncated.

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.

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.

Terminal window
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 to POST /v1/media with kind=audio and Content-Type: audio/ogg, or name a source_url that serves audio/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.

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.

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.