---
title: "Upload a media file (image, video, or PDF carousel)"
method: POST
path: "/media/upload"
tags: ["Media"]
---

# Upload a media file (image, video, or PDF carousel)

`POST /media/upload`

Upload an image or video, or a PDF. A PDF is rasterized into one image slide per page and returned as a carousel (see PdfUploadResult): pass all of `media_ids` to POST /posts. On LinkedIn the slides post as a native swipeable document; on Instagram, TikTok, Threads and Pinterest as an image carousel. PDFs are capped at 20 pages.

## Response `201`

File uploaded successfully. For an image or video the body has a single `data` (Media). For a PDF the body is a PdfUploadResult with `slides` + `media_ids` for the whole carousel.

- union
  - object
    - `data` Media
      - `id` string — Media ID (use this in post creation)
      - `url` string — Permanent CDN URL for the media
      - `thumbnail_url` string, nullable — CDN URL of the generated preview thumbnail (videos only; null for images, which use `url` directly)
      - `type` 'image' | 'video'
      - `name` string, nullable — Human-readable label set on upload or via PATCH (e.g. "pp-play5get50"). Falls back to the storage filename when unset. Search this with the `search` query param.
      - `folder_id` string, nullable — Id of the folder this file lives in, or null for the root ("All media")
      - `filename` string — Generated storage filename (stable, used internally)
      - `size` string — Human-formatted file size (e.g. "2.50 MB")
      - `status` 'ready' | 'processing' | 'failed' — Processing state. 'ready' for normal uploads (usable immediately). Large videos uploaded via URL ingestion (over 100 MB) start as 'processing' — poll get media / list media until 'ready' before using them in a post. 'failed' means the source couldn't be fetched or validated.
      - `created_at` string, date-time
    - `compatibility` MediaCompatibility — Whether the file fits the platforms connected to your workspace. `compatible` is true when no connected platform would reject it.
      - `compatible` boolean
      - `warnings` object[]
        - `platform` string
        - `display_name` string
        - `reasons` string[] — e.g. ["max 300 MB"], ["format not supported"]
      - `summary` string, nullable — One-line human summary, or null when fully compatible.
    - `message` string
  - PdfUploadResult — Returned when the uploaded file is a PDF. A PDF only renders natively on LinkedIn, so the server rasterizes each page into an image slide and stores each as its own media item. `data` mirrors the FIRST slide (so callers that read a single Media still work); `slides` and `media_ids` carry the whole carousel in page order. Pass ALL of `media_ids` to POST /posts as a carousel. On LinkedIn it posts as a swipeable document; on Instagram, TikTok, Threads and Pinterest as an image carousel. Capped at 20 pages.
    - `data` Media
      - `id` string — Media ID (use this in post creation)
      - `url` string — Permanent CDN URL for the media
      - `thumbnail_url` string, nullable — CDN URL of the generated preview thumbnail (videos only; null for images, which use `url` directly)
      - `type` 'image' | 'video'
      - `name` string, nullable — Human-readable label set on upload or via PATCH (e.g. "pp-play5get50"). Falls back to the storage filename when unset. Search this with the `search` query param.
      - `folder_id` string, nullable — Id of the folder this file lives in, or null for the root ("All media")
      - `filename` string — Generated storage filename (stable, used internally)
      - `size` string — Human-formatted file size (e.g. "2.50 MB")
      - `status` 'ready' | 'processing' | 'failed' — Processing state. 'ready' for normal uploads (usable immediately). Large videos uploaded via URL ingestion (over 100 MB) start as 'processing' — poll get media / list media until 'ready' before using them in a post. 'failed' means the source couldn't be fetched or validated.
      - `created_at` string, date-time
    - `slides` Media[] — Every page as a Media item, in page order.
      - `id` string — Media ID (use this in post creation)
      - `url` string — Permanent CDN URL for the media
      - `thumbnail_url` string, nullable — CDN URL of the generated preview thumbnail (videos only; null for images, which use `url` directly)
      - `type` 'image' | 'video'
      - `name` string, nullable — Human-readable label set on upload or via PATCH (e.g. "pp-play5get50"). Falls back to the storage filename when unset. Search this with the `search` query param.
      - `folder_id` string, nullable — Id of the folder this file lives in, or null for the root ("All media")
      - `filename` string — Generated storage filename (stable, used internally)
      - `size` string — Human-formatted file size (e.g. "2.50 MB")
      - `status` 'ready' | 'processing' | 'failed' — Processing state. 'ready' for normal uploads (usable immediately). Large videos uploaded via URL ingestion (over 100 MB) start as 'processing' — poll get media / list media until 'ready' before using them in a post. 'failed' means the source couldn't be fetched or validated.
      - `created_at` string, date-time
    - `media_ids` string[] — Slide ids in page order — pass all of them to POST /posts.
    - `pdf` object
      - `total_pages` integer
      - `rendered_pages` integer
      - `truncated` boolean — True when the PDF had more than 20 pages and only the first 20 were imported.
    - `compatibility` MediaCompatibility — Whether the file fits the platforms connected to your workspace. `compatible` is true when no connected platform would reject it.
      - `compatible` boolean
      - `warnings` object[]
        - `platform` string
        - `display_name` string
        - `reasons` string[] — e.g. ["max 300 MB"], ["format not supported"]
      - `summary` string, nullable — One-line human summary, or null when fully compatible.
    - `message` string

## Other responses

- `400` — Invalid file or missing file
- `413` — Payload too large. This endpoint is hard-capped at 100MB and bodies above that are rejected by our CDN (Cloudflare) BEFORE reaching the API, so the response is a NON-JSON HTML page, not the usual JSON error envelope. To upload files up to 1GB, use POST /media/upload-from-url instead (the server fetches the URL and bypasses the body cap).

---

[API](https://skmtc.dev/omnisocials/apis/omnisocials-api.md) · [All operations](https://skmtc.dev/omnisocials/apis/omnisocials-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/omnisocials/omnisocials-api/revisions/e53b89af0b98/schema)
