> ## Documentation Index
> Fetch the complete documentation index at: https://docs.social-api.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Base URL: https://api.social-api.ai/v1. Send your API key in the Authorization header: Authorization: Bearer <your API key>.
> Every error response from /v1 is JSON: { "error": { "code", "message", "meta" (optional) }, "request_id" }. Branch on error.code; use error.message only for logs.
> A 429 response carries a Retry-After header, in seconds, when the limit has a known wait. Authenticated responses can carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for the per-minute request budget your SocialAPI account shares across every API key (see /guides/rate-limits).
> Paginated list endpoints return next_cursor, at the top level or inside pagination (see /guides/pagination). Pass it as the cursor query parameter to get the next page. An absent or empty next_cursor means you have the last page.
> Treat every ID as an opaque string: do not parse it or assume a format.
> Support differs by platform and by operation. Check /platforms/overview before assuming an operation works on a platform. An unsupported operation returns 501 with error.code resource.not_supported.

# Upload media

> Upload images and videos to your SocialAPI media library with one request or a presigned URL, or attach a public URL or platform attachment ID to a post.

Media in SocialAPI is the images and videos you attach to a post. A post carries an ordered `media` array, and each item names where the file comes from: your media library, a public URL, or a media ID you created on the platform. Order decides carousel sequence.

Uploading, verifying, and listing media cost no credits. A credit is charged when a post is published or scheduled. Storage counts against your plan: Free has 100 MB, and every paid plan is unlimited. See [Plans and usage](https://docs.social-api.ai/guides/plans.md#plan-allowances).

## Endpoints

| Method | Path | Description |
| - | - | - |
| `POST` | `/v1/media/upload` | Upload a file of up to 50 MB in one request |
| `GET` | `/v1/media/upload-url` | Get a signed URL to upload a larger file |
| `POST` | `/v1/media/{id}/verify` | Confirm a signed-URL upload |
| `GET` | `/v1/media` | List the files in your library |
| `DELETE` | `/v1/media/{id}` | Delete a file |
| `GET` | `/v1/media/storage` | Read your storage use and quota |

## Media sources

Each item of `media` has a `source_type` and a `source`:

| `source_type` | `source` is | Use it when |
| - | - | - |
| `media_id` | The ID of a file in your library | You uploaded the file to SocialAPI |
| `url` | A public `http` or `https` URL | The file is already hosted and you do not want to upload it |
| `platform_attachment_id` | A media ID from the platform's own API | You uploaded the file to the platform yourself. Add `type`: `image`, `video`, `audio`, or `file` |

```json theme={null}
{
  "text": "Behind the scenes from our studio shoot.",
  "media": [
    { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" },
    { "source_type": "url", "source": "https://cdn.example.com/photo.jpg" }
  ],
  "platform_data": { "instagram": { "content_type": "carousel" } },
  "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }]
}
```

`media` is also accepted on a target, where it replaces the post's list for that target. A target without `media` uses the post's list.

The older `media_ids` array still works: `["abc"]` equals `[{"source_type": "media_id", "source": "abc"}]`. When you send both, `media` wins. Responses return `media_ids` next to `media`, listing the `media_id` items only.

## Choose an upload path

| | Server-side upload | Signed URL upload |
| - | - | - |
| Requests | 1 | 3 |
| Largest file | 50 MB | No SocialAPI limit. Your storage quota and the platform's limits apply |
| Use it for | Small files, and clients that cannot call another host (AI sandboxes) | Large files, especially video |

## Upload with one request

Send the file as multipart form data in the `file` field:

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/media/upload \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -F "file=@photo.jpg"
```

Response (HTTP 201):

```json theme={null}
{
  "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
```

The file is `ready` at once. A request without `file` returns `400 validation.field_required`. A file over 50 MB returns `400 validation.field_invalid`.

## Upload with a signed URL

**1. Request a signed URL**

Send the file's MIME type as `media_type` and its name as `filename`. Both are required.

```bash theme={null}
curl "https://api.social-api.ai/v1/media/upload-url?media_type=video/mp4&filename=reel.mp4" \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Response (HTTP 200):

```json theme={null}
{
  "media_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "upload_url": "https://storage.example.com/media/reel.mp4?X-Amz-Signature=abc123",
  "expires_at": "2026-10-13T09:15:00Z"
}
```

The response has no public URL: the file becomes usable through its `media_id`. `upload_url` stops working at `expires_at`, 15 minutes after the request. The media is `pending`.

**2. Upload the bytes**

Send the file with `PUT` to `upload_url`. The `Content-Type` header must equal the `media_type` from step 1.

```bash theme={null}
curl -X PUT "https://storage.example.com/media/reel.mp4?X-Amz-Signature=abc123" \
  -H "Content-Type: video/mp4" \
  --data-binary @reel.mp4
```

**3. Verify the upload**

Verification is required. It confirms the file arrived, records its size, and makes the media `ready`.

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/media/f47ac10b-58cc-4372-a567-0e02b2c3d479/verify \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Response (HTTP 200):

```json theme={null}
{
  "success": true
}
```

If the file is not at the upload URL when you verify, the media becomes `failed` and the request returns `404 media.not_found`. If the file would take you over your storage quota, SocialAPI deletes it, the media becomes `failed`, and the request returns `413 billing.storage_quota`.

Media has 3 statuses: `pending` (created, bytes not confirmed), `ready` (usable in posts), and `failed`. Only `ready` media publishes. A post that names media that is not `ready` returns `400 publishing.validation_failed`, as [Track post delivery](https://docs.social-api.ai/guides/delivery.md#verify-media-before-you-publish) describes.

## Attach media to a post

Pass `media` on `POST /v1/posts`. One item is an image or a video, and 2 or more items are a carousel, within each platform's limits.

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/posts \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Behind the scenes from our studio shoot.",
    "publish_now": true,
    "media": [
      { "source_type": "media_id", "source": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }
    ],
    "platform_data": { "instagram": { "content_type": "feed" } },
    "targets": [{ "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" }]
  }'
```

The formats, sizes, and counts each platform accepts are on its publishing page. `GET /v1/posts/validate` returns them as data. See [Publish a post](https://docs.social-api.ai/guides/publishing.md#validate-before-you-publish).

## Publish from a public URL

A `url` source is not stored in your library and uses no storage quota.

```json theme={null}
{
  "media": [
    { "source_type": "url", "source": "https://cdn.example.com/photo.jpg" }
  ]
}
```

* **Reachable:** the URL must be a public `http` or `https` address when you create the post and again when the post publishes. A URL that resolves to a private, loopback, or link-local address returns `400 validation.field_invalid`. A post scheduled for later fails when the URL stops working before it publishes.
* **Typed:** SocialAPI reads the file type from the URL. When it cannot, the request returns `422 publishing.media.untyped_url`. Use a URL with an extension such as `.jpg` or `.mp4`, or upload the file.
* **Lookup failure:** when the host cannot be looked up for a moment, the request returns `503 system.internal`. Send it again.

For a post scheduled far ahead, upload the file and use a `media_id`: a link that expires or changes before the post publishes fails the post.

## Publish a platform attachment ID

If you uploaded media through a platform's own API, pass its ID and SocialAPI publishes it without a second upload. `type` is required.

```json theme={null}
{
  "media": [
    { "source_type": "platform_attachment_id", "source": "18234567890123456", "type": "image" }
  ]
}
```

Instagram, Threads, LinkedIn, and X accept `platform_attachment_id`. Any other platform returns `422 publishing.media.unsupported_source` for that target. A missing `type` returns `422 publishing.media.missing_type`.

| Platform | Mix an attachment ID with other media |
| - | - |
| LinkedIn, X | Yes. Combine it freely with `url` and `media_id` items |
| Instagram, Threads | No. The attachment ID must be the only item in `media`, because it is one finished photo, video, or carousel. More items return `422 publishing.media.mixing_unsupported` |

These errors apply per target: the post still publishes to its other targets. To build a mixed carousel on Instagram or Threads, send `url` or `media_id` items.

## Manage your library

`GET /v1/media` lists your `ready` files, newest first. Each item has `id`, `filename`, `content_type`, `size_bytes`, `status`, `created_at`, and a `url` for previewing the file. The `url` stops working after 2 hours. `limit` is 1 to 100 and defaults to 50. `cursor` is the `next_cursor` of the previous page. See [Pagination](https://docs.social-api.ai/guides/pagination.md).

```bash theme={null}
curl "https://api.social-api.ai/v1/media?limit=50" \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

`DELETE /v1/media/{id}` deletes a file and returns HTTP 204. SocialAPI does not check whether a post uses the file, so a scheduled post that uses a deleted file fails when it publishes. A file that does not exist returns `404 media.not_found`.

`GET /v1/media/storage` returns your storage use:

```bash theme={null}
curl https://api.social-api.ai/v1/media/storage \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Response (HTTP 200):

```json theme={null}
{
  "used_bytes": 52428800,
  "limit_bytes": 104857600,
  "count": 12
}
```

`limit_bytes` is `-1` when your plan has no limit. `GET /v1/media/storage`, `GET /v1/media/upload-url`, `POST /v1/media/upload`, and `POST /v1/media/{id}/verify` also return the `X-Storage-Used` and `X-Storage-Limit` headers. Only `ready` files count.

## Limits

| Limit | Value |
| - | - |
| Server-side upload | 50 MB per file. Larger returns `400 validation.field_invalid` |
| Signed upload URL | Valid for 15 minutes |
| Storage, Free plan | 100 MB. An upload over it returns `413 billing.storage_quota` |
| Storage, paid plans | Unlimited |
| Library list | 100 files per page |
| Library preview `url` | Valid for 2 hours |

SocialAPI reads the duration of a video when the upload completes. You do not send it.

## Scopes

| Scope | Grants |
| - | - |
| `media:read` | List the library |
| `media:write` | Upload, get a signed URL, verify, and delete media |

`GET /v1/media/storage` needs no scope: any valid API key can call it.

## Use from an AI agent

| Tool | What it does |
| - | - |
| `social_api_get_media_upload_url` | Gets a signed URL and a `media_id` |
| `social_api_verify_media_upload` | Confirms the upload |
| `social_api_list_media` | Lists the library |
| `social_api_delete_media` | Deletes a file |
| `social_api_get_storage_usage` | Reads storage use and quota |

An agent uploads with the signed URL. The server-side upload has no tool. See [MCP tools](https://docs.social-api.ai/integrations/tools.md).

## Related

* [Publish a post](https://docs.social-api.ai/guides/publishing.md): the `media` field and validation
* [Track post delivery](https://docs.social-api.ai/guides/delivery.md): media status and delivery errors
* [Plans and usage](https://docs.social-api.ai/guides/plans.md): storage allowance for each plan
* [Pagination](https://docs.social-api.ai/guides/pagination.md): cursor lists
* [Errors](https://docs.social-api.ai/guides/errors.md): every media error code


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.