# Media Uploads (/docs/guides/media-uploads)



Media goes in two steps: add the file with `POST /media`, then pass its `id` in `mediaIds` when you create or update a post.

Import from a URL [#import-from-a-url]

The simplest way, and the right one for agents and scripts that already have a link. FeedHorizon downloads the file and answers once it is stored.

```bash title="cURL"
curl -X POST https://feedhorizon.com/api/v1/media \
  -H "Authorization: Bearer fh_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/launch.jpg" }'
```

```json
{
  "data": {
    "id": "med_0193f…",
    "url": "https://…/media/0193f….jpg",
    "filename": "launch.jpg",
    "mimeType": "image/jpeg",
    "sizeBytes": 245760,
    "createdAt": "2026-11-04T14:00:00.000Z",
    "upload": null
  }
}
```

The URL must be public `https` and the file at most 100 MB. The type comes from the `Content-Type` the server sends.

Upload directly [#upload-directly]

For files on your machine and anything over 100 MB (up to 5 GB). Announce the file, then `PUT` the bytes to the returned link. The link is valid for two hours and needs no API key.

```bash title="cURL"
curl -X POST https://feedhorizon.com/api/v1/media \
  -H "Authorization: Bearer fh_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "teaser.mp4", "mimeType": "video/mp4", "sizeBytes": 48211337 }'

# then, with upload.url from the answer:
curl -X PUT "UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @teaser.mp4
```

Use media in a post [#use-media-in-a-post]

```json
{
  "profileId": "YOUR_PROFILE_ID",
  "content": "The teaser is out",
  "platforms": ["instagram", "linkedin"],
  "mediaIds": ["0193f…"],
  "addToQueue": true
}
```

Files appear in the post in the order of `mediaIds`. On update, `mediaIds` replaces the media of the post, and `[]` removes it.

Before the post is stored, each platform's rules are checked: supported types and how many files it takes (see `GET /platforms`). Instagram needs at least one image or video. A file that was announced but never uploaded answers `400 media_not_uploaded`. Use [`POST /posts/validate`](/docs/guides/scheduling#check-before-you-create) to see all problems before you create the post.

Supported types [#supported-types]

`POST /media` stores any of these files:

| Type      | MIME types                                           |
| --------- | ---------------------------------------------------- |
| Images    | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
| Video     | `video/mp4`, `video/quicktime`, `video/webm`         |
| Documents | `application/pdf`                                    |

Which of them a post may use depends on the platform. A file a platform does not take fails the post with `media_type_unsupported`:

| Platform  | Images                           | Video          | Other |
| --------- | -------------------------------- | -------------- | ----- |
| X         | JPEG, PNG, GIF                   | MP4            |       |
| LinkedIn  | JPEG, PNG, GIF                   | MP4            | PDF   |
| Instagram | JPEG, PNG                        | MP4            |       |
| Bluesky   | JPEG, PNG, WebP, GIF (1 MB each) |                |       |
| Pinterest | JPEG, PNG, WebP (20 MB each)     |                |       |
| TikTok    |                                  | MP4, MOV, WebM |       |

Counts and text limits are on [Platform Support](/docs/platforms/overview) and in `GET /platforms`.

Errors [#errors]

| Code                            | Cause                                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------------ |
| `invalid_url`                   | Not a valid `https` URL, a URL with credentials, or a URL that points to a private network |
| `media_fetch_failed`            | The URL could not be resolved or fetched, or answered with an error status                 |
| `media_too_large`               | An import over 100 MB, or an upload over 5 GB                                              |
| `media_not_found` (on posts)    | A `mediaIds` entry does not exist in your organization                                     |
| `duplicate_media` (on posts)    | The same id appears twice in `mediaIds`                                                    |
| `media_not_uploaded` (on posts) | The file was announced but its bytes never arrived                                         |
| `media_in_use` (`409`)          | `DELETE /media/{id}` on a file a post still uses                                           |

Manage files [#manage-files]

* `GET /media` lists your files, newest first.
* `GET /media/{id}` returns one file.
* `DELETE /media/{id}` deletes a file that no post uses. Files still in a post answer `409 media_in_use`.

Media needs the `posts:write` scope to add and delete, `posts:read` to list.
