# Media: photos and videos

Each item can carry up to 10 media in `media[]`. Only **photos and videos** are supported (no GIF animations, documents, audio or voice).

```json
"media": [
  { "type": "photo", "url": "https://example.com/cover.jpg" },
  { "type": "video", "upload_id": "upl_…", "description": "Short screen recording: the bot formats a voice note into a post" }
]
```

Each entry has `type` (`"photo"` or `"video"`), **exactly one** of `url` or `upload_id`, and an optional `description` (up to 1,000 characters).

## Two ways to pass a file

| Way | How | When |
|---|---|---|
| `url` | A public **https** link to the file itself (not to a page with the file). PostFrog downloads it when the item is processed. | The file is already online. The only way in clients without file access (Claude.ai, ChatGPT). |
| `upload_id` | Upload the file first: MCP `create_upload` gives a ready `curl` command for `POST https://bot.postfrog.site/api/v1/uploads`; the response has `upload_id`. | A local file and you can run shell commands (Claude Code, scripts, n8n). |

Upload details:

- `multipart/form-data` with a `type` field (`photo` or `video`) **before** the `file` field. Response HTTP 201: `{ "upload_id", "type", "bytes", "expires_at", "width"?, "height"? }`.
- An `upload_id` lives 24 hours, belongs to the key's account and can be reused in several items. The `type` in `media[]` must match the uploaded type.
- If `get_account` shows `media_uploads_enabled: false`, media are switched off on this server: send posts without media.
- Upload errors: 400 `invalid_request` (wrong format or form), 408 `request_timeout` (the body was too slow, see below), 413 `payload_too_large`, 422 `limit_exceeded` (daily quota: `details.upload_bytes_left_today`, `details.uploads_left_today`), 429 `rate_limited` (wait `Retry-After`), 502 `media_store_failed` (Telegram did not store the file: retry the upload later), 503 `media_disabled` (media are switched off on this server: send posts without media).

## Media of the Threads version

`threads[].media` (see `modes`) takes only public **https** links to the file itself: `{ "type": "photo" | "video", "url" }`, no `upload_id` and no `description`. PostFrog does not download or store these files: the link is handed over when the author publishes the Threads version, so it must stay reachable until then. These media do not count towards `media[]` and do not appear in the Telegram post.

## Formats and sizes

| Type | Formats | Max size |
|---|---|---|
| photo | JPEG, PNG, WebP | 10 MB |
| video | MP4 or MOV with H.264/HEVC video and AAC audio | 50 MB |

- The format is checked by the file's content, not by its name or Content-Type. WebM, GIF, HEIC, AVIF and SVG are rejected.
- Uploads and downloaded URLs together share a daily quota of 500 MB and 300 files per author (UTC day).

## Speed limits

Media storage is shared by all authors, so slow transfers are cut:

- All media of one item must be downloaded and stored within 120 seconds. Media still missing after that are skipped with `media_too_slow` (files from `upload_id` are not affected: they are already stored).
- A file that arrives slower than 32 KB/s for a while is cut with `media_too_slow`. Use a fast host (a CDN, not a home server).
- An upload body that stops for 60 seconds, or takes longer than 180 seconds to send, is answered with HTTP 408 `request_timeout`. Retry on a better connection.
- Many uploads in a row from one author are paced: the response simply comes a bit later.

## Placement in the post

- Media are numbered in the order of `media[]`, **starting at 0**. In a Rich post, a line `%%MEDIA:0%%` puts the first media at that spot.
- `%%MEDIA:0,1,2%%` - three or more media in one line become a **collage**. Two in one line are shown one after another.
- Use each index once, each placeholder on its own line with blank lines around it.
- Media without a placeholder go to the **top of the post** as a cover (three or more of them as a collage). So for a simple "photo on top" post you do not need any placeholder.
- In `exact` mode a placeholder pointing past the end of `media[]` is the error `media_index_out_of_range`. Pass `media_count` (or `media`) to `validate_post` to check it.
- In Markdown mode media always sit on top (a caption post, or an album for several) and placeholders are ignored. The text must then fit the caption limit, see `formatting`.

## What PostFrog's AI sees

In `format` and `write` the AI does **not watch videos**. Describe every video in `description` (what happens, who speaks, the key point) so the post can talk about it. A short description of photos helps too.

## When a file fails

A broken media entry never fails the whole item: it is skipped, the order of the others is kept, and the item gets a warning `{ "code", "message": "media[i]: …" }` where `i` is the index in your `media[]`:

| Code | Meaning | What to do |
|---|---|---|
| `media_fetch_failed` | The URL could not be downloaded: not https, private address, DNS error, HTTP error, timeout | Check that the link is a public https link to the file |
| `media_too_large` | Larger than the limit of its type | Compress or shorten it |
| `media_bad_type` | Not a supported format, or Telegram did not accept it as that type | Convert: JPEG for photos, MP4 (H.264 + AAC) for videos |
| `media_quota_exceeded` | The author's daily media quota (bytes or number of files) is used up | Send tomorrow (UTC) or without media |
| `media_too_slow` | The file came too slowly, or all media of the item took longer than 120 seconds | Host the file on a faster server, send fewer or smaller media, or upload it and use `upload_id` |
| `media_store_failed` | Telegram did not store the file | Retry later |
| `media_disabled` | Media are switched off on this server | Send without media |
| `upload_expired` | The `upload_id` is older than 24 hours | Upload again |
| `upload_not_found` | No such `upload_id` for this account | Use an id from your own upload |
| `media_too_many` | More media than allowed | Send at most 10 |

`send_posts` already rejects an `upload_id` that is unknown, expired, not finished or of another type (HTTP 400 with the path `items.N.media.M.upload_id`), so those warnings are rare.