---
title: "Get signed media upload URL"
method: POST
path: "/api/v1/media/signed-upload"
tags: ["Media"]
---

# Get signed media upload URL

`POST /api/v1/media/signed-upload`

## Request

- Base URL: `https://www.poster.ly`
- URL: `https://www.poster.ly/api/v1/media/signed-upload`
- Auth: HTTP bearer

Preferred upload flow for larger image, video and PDF files. Accepted types: JPEG, PNG, GIF, WebP, MP4, MOV, WebM, and PDF up to 50MB (a PDF is for LinkedIn document posts only). HEIC and every other type are rejected. Returns a short-lived object-storage signed URL. Handles 1 to 20 files per call: send `files` for a batch and get `slots` plus a `summary` back, with per-file errors reported in place and the storage quota counted across the batch. A batch spends one point of the media rate limit per file (a single-file call spends one), against the media upload limit, so a request that does not fit the remaining budget answers `429` with `Retry-After`. Slots expire after 15 minutes. PUT the raw file bytes to `upload_url` with the returned `headers` (at least the signed Content-Type). Send `pin_content_type: true` to lock each URL to the declared type: the PUT must then send exactly the returned `Content-Type` header or storage answers 403 (curl --data-binary needs -H "Content-Type: ..."). After a successful PUT, pass `public_url` to create_post as media_url. The file never travels through a posterly serverless function, so plan caps (Starter 500MB video, Agency 4GB) are not limited by Vercel payload size. To publish a PDF, create the post with `post_type: "document"`, set `media_url` to the PDF `public_url` and pass `document_title` (required, up to 100 characters) in `settings`; only LinkedIn publishes documents.

## Request body

- union: Send either the single-file fields (filename, content_type, size) or `files` for a batch of 1 to 20. Mixing the two answers 400.
  - MediaSignedUploadSingleRequest
    - `filename` string, required
    - `content_type` string, required
    - `size` integer, required
    - `pin_content_type` boolean: Optional. true signs the Content-Type into each upload URL, so the PUT must send exactly the returned `Content-Type` header or storage answers 403 SignatureDoesNotMatch (curl --data-binary sends the wrong type unless you pass -H "Content-Type: ..."). posterly may turn it on by default in the future, so always send the returned `Content-Type`; a pinned slot carries `content_type_pinned: true`. The media drop page always pins. Only R2 storage can pin; elsewhere the response says `content_type_pinned: false`.
    - `workspace_id` string: Optional. The workspace this call acts in and whose plan and credits it uses (from whoami). Needed only when the key can act in several workspaces.
  - MediaSignedUploadBatchRequest
    - `files` object[], required: One entry per file, 1 to 20. Each file is validated on its own and the storage quota is counted across the whole batch.
      - `filename` string, required
      - `content_type` string, required
      - `size` integer, required
    - `pin_content_type` boolean: Optional. true signs the Content-Type into each upload URL, so the PUT must send exactly the returned `Content-Type` header or storage answers 403 SignatureDoesNotMatch (curl --data-binary sends the wrong type unless you pass -H "Content-Type: ..."). posterly may turn it on by default in the future, so always send the returned `Content-Type`; a pinned slot carries `content_type_pinned: true`. The media drop page always pins. Only R2 storage can pin; elsewhere the response says `content_type_pinned: false`.
    - `workspace_id` string: Optional. The workspace this call acts in and whose plan and credits it uses (from whoami). Needed only when the key can act in several workspaces.

## Response `200`

Signed upload URL generated (single file), or one slot per file with a summary (batch)

- union
  - MediaSignedUploadResponse
    - `upload_url` string, required: Object-storage signed PUT URL. PUT raw bytes here; do not send the file through a posterly API route.
    - `token` string, required
    - `path` string, required
    - `public_url` string, required
    - `expires_at` string, date-time
    - `max_size` integer
    - `headers` object: Headers the follow-up PUT must send. Always include the signed content-type.
    - `provider` 'r2' | 'supabase'
    - `content_type_pinned` boolean: Present only when pinning was requested or is on by default. true: the PUT must send exactly the returned Content-Type header or storage answers 403.
  - MediaSignedUploadBatchResponse
    - `slots` object[], required: One entry per requested file, in request order. A file that fails has ok: false with an error and code; the other files still get slots.
      - `index` integer, required: Position of the file in the request.
      - `filename` string, required
      - `ok` boolean, required
      - `upload_url` string: Present when ok is true. Object-storage signed PUT URL.
      - `token` string
      - `path` string
      - `public_url` string
      - `expires_at` string, date-time
      - `max_size` integer
      - `headers` object
      - `provider` 'r2' | 'supabase'
      - `content_type_pinned` boolean: Present only when pinning was requested or is on by default. true: the PUT must send exactly the returned Content-Type header or storage answers 403.
      - `error` string: Present when ok is false.
      - `code` string: Present when ok is false, for example file_too_large, unsupported_file_type or trial_media_storage_limit.
    - `summary` object, required
      - `requested` integer, required
      - `ok` integer, required
      - `failed` integer, required

## Other responses

- `400`: Validation error
- `401`: Invalid or missing API key
- `429`: Too many requests

---

[API](https://skmtc.dev/poster/apis/posterly-api.md) · [All operations](https://skmtc.dev/poster/apis/posterly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/poster/apis/posterly-api/revisions/60be02a61f95?raw)
