# Upload Images, Video and PDFs via API

import { TechArticle } from "../components/TechArticle";

<TechArticle slug="media" fm={frontmatter} />

Upload and manage media files for use in posts. Every media file is stored in your workspace's media library and can be referenced by multiple posts.

## Upload a file

Use `POST /media/upload` with a multipart form. Maximum file size is **100 MB** for a direct upload. To ingest larger files (up to **1 GB**), use [Upload from a URL](#upload-from-a-url), which downloads asynchronously.

```bash
curl -X POST https://api.omnisocials.com/v1/media/upload \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -F "file=@photo.jpg"
```

Response:

```json
{
  "data": {
    "id": "123",
    "url": "https://cdn.omnisocials.com/workspace-42/photo.jpg",
    "thumbnail_url": null,
    "type": "image",
    "name": "photo.jpg",
    "filename": "photo-1a2b3c.jpg",
    "folder_id": null,
    "size": "0.80 MB",
    "status": "ready",
    "created_at": "2026-04-10T14:22:00Z"
  },
  "compatibility": { "overall": "ok", "platforms": {} },
  "message": "File uploaded successfully. Use the 'id' in your post request."
}
```

The media record is nested under `data`. Note that `size` is a human-readable string (e.g. `"0.80 MB"`), not a byte count. The `compatibility` block is a preflight check of the file against your connected platforms. Save `data.id`; you reference media on posts through the `media_ids` field.

`status` is `ready` for normal uploads. Large asynchronous URL ingests start as `processing`; poll `GET /media/:id` until `status` is `ready` before using the file in a post. A source that can't be fetched or validated becomes `failed`.

## Upload from a URL

Skip the download-and-reupload dance by pointing OmniSocials at a public URL. The server fetches the file (up to 1 GB) and stores it in your media library.

```bash
curl -X POST https://api.omnisocials.com/v1/media/upload-from-url \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/photo.jpg" }'
```

The response shape matches `POST /media/upload`. Files above ~100 MB are ingested asynchronously and come back with `status: "processing"`.

## Upload a PDF as a carousel

Upload a PDF and OmniSocials rasterizes it into one image slide per page (up to 20 pages), then returns a media ID for every slide. Pass **all** of the returned IDs to `media_ids` on a post to publish the deck as a carousel: on LinkedIn it posts as a native swipeable document; on Instagram, TikTok, Threads and Pinterest as an image carousel.

PDFs are accepted on every upload path: `POST /media/upload`, `upload-from-url`, `upload-from-base64`, the presigned `upload-by-token` flow, and `media_urls` on a post (where the PDF expands in place into one slide per page). The upload response carries the whole set:

```json
{
  "data": { "id": "201", "type": "image", "status": "ready" },
  "slides": [ { "id": "201" }, { "id": "202" }, { "id": "203" } ],
  "media_ids": ["201", "202", "203"],
  "pdf": { "totalPages": 3, "renderedPages": 3, "truncated": false }
}
```

`data` mirrors the first slide for backward compatibility; use `media_ids` to attach the full carousel.

## Inline URLs on a post

If you only need the media for a single post, skip the media library entirely. Pass URLs directly in `media_urls` on the post body. Note the size split: `media_urls` on a post caps at 100 MB per file, while `upload-from-url` handles up to 1 GB; for big videos, upload first and pass the returned media id.

```json
{
  "content": { "default": "Check this out" },
  "accounts": ["your-account-id"],
  "media_urls": ["https://example.com/photo.jpg"]
}
```

The API downloads, validates, and attaches the file automatically. The maximum number of items is platform-dependent (Instagram/Threads up to 10, TikTok up to 35, X/Bluesky/Mastodon up to 4, Pinterest up to 5, Google Business 1). Use the media library (via `upload` or `upload-from-url`) when the same file appears on multiple posts, to save on download bandwidth.

Any entry, in `media_urls` or `media_ids`, can be an object instead of a bare string to carry an accessibility description (alt text, max 1500 characters): `{ "url": "https://example.com/bike.jpg", "alt": "A red bicycle leaning against a brick wall" }` (or `{ "id": "123", "alt": "..." }` for library media). See [Attaching media](/creating-posts#attaching-media) for which platforms receive it.

## Presigned uploads

When the uploading party cannot send an `Authorization` header (a browser form, an AI sandbox, a third-party service), mint a one-time upload URL instead of sharing your API key:

```bash
curl -X POST https://api.omnisocials.com/v1/media/create-upload-url \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

The response contains an `upload_url` of the form `https://api.omnisocials.com/v1/media/upload-by-token?upload_token=...`. POST a multipart form with a `file` field to that URL, with no auth header; the token in the URL is the auth. Tokens are single-use and expire after 10 minutes. The upload response matches `POST /media/upload` (including the PDF `media_ids` expansion).

## Folders

Organize the library into folders. Reading uses `media:read`, writing uses `media:write`.

| Endpoint | Description |
|----------|-------------|
| `GET /folders` | List folders |
| `POST /folders` | Create a folder |
| `PATCH /folders/:id` | Rename a folder |
| `DELETE /folders/:id` | Delete a folder |

Move a file into a folder with `PATCH /media/:id` and a `folder_id`; filter the library by folder with `GET /media?folder_id=`.

## Searching the library

`GET /media` takes four optional query parameters:

| Parameter | Description |
|-----------|-------------|
| `limit` | Items per page, 1 to 100 (default 20) |
| `offset` | Skip this many items |
| `search` | Case-insensitive match on the media name and filename |
| `folder_id` | Only items in this folder |

Use `search` to find an existing asset by name instead of re-uploading it.

## Supported formats

| Media type | Formats |
|------------|---------|
| Images | JPEG, PNG, GIF, WebP, HEIC/HEIF (converted to JPEG on upload) |
| Videos | MP4, MOV, AVI |
| Documents | PDF (rasterized into image slides; see above) |

Platform-specific size, aspect ratio, and duration requirements are enforced at publish time, not upload time. A video can upload successfully and still fail to publish to Instagram if it exceeds Instagram's length limit. See the [Platforms](/platforms) section for per-platform media specs.

## Managing media

| Endpoint | Description |
|----------|-------------|
| `GET /media` | List everything in the library |
| `GET /media/:id` | Fetch a single media record |
| `PATCH /media/:id` | Rename a media file or move it to a folder |
| `DELETE /media/:id` | Delete from the library |
| `POST /media/check` | Preflight-check a file's compatibility with your connected platforms |
| `POST /media/upload-from-base64` | Upload a base64-encoded file |
| `POST /media/create-upload-url` | Mint a one-time presigned upload URL (see above) |

Reading media (`GET /media`, `GET /media/:id`) requires the `media:read` scope; all writes require `media:write`.

Deleting a media file that is still attached to a scheduled or in-progress post returns `409 media_in_use`. Deleting one that is only on already-published posts succeeds and does not remove it from those live posts.
