> ## Documentation Index
> Fetch the complete documentation index at: https://apidoc.bulkneo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending media

> There is no upload endpoint — you host the file, we fetch the link. What makes a link work, and what makes it fail.

Images, videos, documents, voice notes and stickers all work the same way: **you give us a public link and we fetch the file from it.**

<Warning>
  **There is no upload endpoint.** You cannot post a file to this API. If your file is not already on the internet somewhere, put it there first.
</Warning>

That is a deliberate scope decision, not an oversight. Hosting uploads would mean storage, quotas, retention rules and virus scanning, none of which exist here yet.

## What a good link looks like

A link works when opening it in a private browser window **downloads the file itself** with no login and no intermediate page.

<Columns cols={2}>
  <Card title="Works" icon="check">
    ```
    https://cdn.example.com/invoices/2043.pdf
    https://example.com/media/photo.jpg
    https://bucket.s3.amazonaws.com/receipt.png?X-Amz-Signature=…
    ```

    A direct link to the bytes. Signed URLs are fine as long as they are still valid at the moment you call the API.
  </Card>

  <Card title="Fails" icon="xmark">
    ```
    https://drive.google.com/file/d/…/view
    https://www.youtube.com/watch?v=…
    https://dropbox.com/s/…?dl=0
    http://192.168.1.10/file.pdf
    ```

    Preview pages, share pages and video sites serve HTML, not a file. Private addresses are refused outright.
  </Card>
</Columns>

## The rules, exactly

| Rule                                       | Why                                                                                                                      | If broken               |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
| Must be `http://` or `https://`            | Nothing else can be fetched                                                                                              | `400 invalid_media_url` |
| Must be publicly reachable                 | We fetch it from the internet, not from your network                                                                     | `400` or `422`          |
| No private, internal or loopback addresses | `10.x`, `192.168.x`, `172.16–31.x`, `127.x`, `169.254.x`, `localhost`, `*.local`, `*.internal`, and the IPv6 equivalents | `400 invalid_media_url` |
| No username or password in the URL         | Credentials in a URL always end up in a log somewhere                                                                    | `400 invalid_media_url` |
| At most 2,048 characters                   | Very long signed URLs can exceed this                                                                                    | `400 invalid_request`   |

Every refusal gives the **same** message — `Field "url" must be a publicly reachable http(s) URL to the file.` — regardless of which rule was broken. That is on purpose: it is actionable either way, and a per-reason message would let someone map a private network from outside by watching which hosts produce which wording.

## The two failures, and how to tell them apart

<AccordionGroup>
  <Accordion title="400 invalid_media_url — we refused to even try" icon="ban">
    The URL failed a check before anything was fetched: wrong scheme, malformed, too long, credentials in it, or a private address.

    **Fix the URL.** Retrying the same one will never work. `details.field` names the field — `url`, or `buttons[N].url` for a link button.
  </Accordion>

  <Accordion title="422 media_fetch_failed — we tried and could not get the file" icon="link-slash">
    The URL looked fine but the file could not be downloaded: a 404, an expired signed link, a hostname that does not resolve, a TLS failure, or a page where a file was expected.

    **Open the link in a private browser window.** If it does not download the raw file for you, it will not for us either.
  </Accordion>
</AccordionGroup>

## Which endpoint for which file

| You are sending                 | Endpoint                                                    | Extra fields          |
| ------------------------------- | ----------------------------------------------------------- | --------------------- |
| A photo                         | [`/v1/messages/image`](/api-reference/messages/image)       | `caption`             |
| A video clip                    | [`/v1/messages/video`](/api-reference/messages/video)       | `caption`             |
| A PDF or any other file         | [`/v1/messages/document`](/api-reference/messages/document) | `filename`, `caption` |
| Audio, arriving as a voice note | [`/v1/messages/audio`](/api-reference/messages/audio)       | none                  |
| A sticker                       | [`/v1/messages/sticker`](/api-reference/messages/sticker)   | none                  |

<Tip>
  **Always set `filename` on a document.** It is the name the recipient sees and saves. Without it the name comes from the URL, which is often something like `download.php` or a signed-URL blob.
</Tip>

## Formats

We do not restrict file types — WhatsApp decides what it can render. In practice:

* **Images** — JPEG and PNG are safest.
* **Video** — MP4 with H.264 video and AAC audio.
* **Audio** — MP3 and OGG/Opus arrive as playable voice notes.
* **Stickers** — WebP. Other formats may arrive as an ordinary image or not render at all.
* **Documents** — anything. PDF is the most predictable across devices.

`mime_type` is an optional hint you can send with image, video and document. It is checked for the shape `type/subtype` only — we never verify it matches the actual file, so a wrong hint is worse than none.

## Sizes

There is no size limit in this API, but there are two real ones underneath:

* **WhatsApp's own attachment limits.** A file that is too big for WhatsApp will fail however it was sent.
* **Time.** The file has to be fetched during your request. A large file over a slow link can exhaust the request budget and come back as `502 upstream_unavailable`.

Keep media small. A 200 KB receipt sends instantly; a 40 MB video is a gamble.

## Keeping links alive

The file is fetched **during your API call**, not later. So:

* A signed URL must still be valid at the moment you send. Generate it right before the call, not the night before.
* Do not delete the file immediately after the API returns `200`. The fetch has already happened by then, but keeping it around a while makes debugging far easier.
* If your storage is private by default, generate a short-lived public link for the send.
