> ## 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.
> 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.

# Publish a post

> Publish, validate, edit, and import posts to Instagram, Facebook, TikTok, YouTube, and more with one POST /v1/posts request and per-platform delivery status.

A post in SocialAPI is one piece of content sent to one or more connected accounts. You create it with `POST /v1/posts`, name each destination in `targets`, and read the delivery result per platform. The same request publishes to Instagram, Facebook, Threads, TikTok, YouTube, LinkedIn, X, Pinterest, Google Business Profile, Zalo, and Bluesky.

Zalo publishes text articles only, and Bluesky publishes text and up to 4 images. What each platform accepts is on its publishing page, linked from [Platform support](#platform-support). A post costs 1 post credit when it is published or scheduled, whatever the number of targets. Saving a draft is free. See [Plans and usage](/guides/plans#what-counts-as-a-post-or-interaction).

## Endpoints

| Method | Path | Description |
| - | - | - |
| `POST` | `/v1/posts` | Create a draft, schedule a post, or publish it now |
| `GET` | `/v1/posts` | List posts. Parameters are in the table below |
| `GET` | `/v1/posts/{pid}` | Read one post with its targets |
| `PATCH` | `/v1/posts/{pid}` | Edit a `draft`, `scheduled`, or `failed` post |
| `POST` | `/v1/posts/{pid}/publish` | Publish a `draft` or `scheduled` post now |
| `POST` | `/v1/posts/{pid}/retry` | Re-send the failed targets of a `failed` or `partial` post |
| `POST` | `/v1/posts/{pid}/unpublish` | Remove the published copies from the platforms and keep the post |
| `DELETE` | `/v1/posts/{pid}` | Delete a post |
| `POST` | `/v1/posts/validate` | Check a post against platform rules without creating it |
| `GET` | `/v1/posts/validate` | Read each platform's text and media rules |
| `POST` | `/v1/posts/import` | Create up to 500 posts from a CSV file |

`GET /v1/posts` takes these query parameters, all optional. Pagination is in [Pagination](/guides/pagination).

| Parameter | Description |
| - | - |
| `status` | `draft`, `scheduled`, `publishing`, `published`, `partial`, `failed`, or `cancelled` |
| `platform` | A platform slug, such as `instagram` |
| `account_ids` | Connected account IDs, comma-separated |
| `page_id` | A platform page ID, for a Facebook account that has more than one Page |
| `brand_id` | Posts with a target in this brand |
| `from`, `to` | RFC 3339 timestamps, both inclusive. They match the post's publish time, else its scheduled time, else its creation time |
| `search` | Case-insensitive substring of the post's `text` |
| `hidden` | `true` also returns posts whose `hidden` field is `true` |
| `sort` | `scheduled_asc` (default), `scheduled_desc`, `created_asc`, or `created_desc` |

## Your first post

Publish one text post to a Threads account and a Facebook Page:

```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": "Spring sale starts today: https://example.com/spring",
    "publish_now": true,
    "targets": [
      {"account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M"},
      {"account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K", "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M"}
    ]
  }'
```

Response (HTTP 201):

```json theme={null}
{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "text": "Spring sale starts today: https://example.com/spring",
  "status": "publishing",
  "hidden": false,
  "retry_count": 0,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z",
  "targets": [
    {
      "account_id": "acc_01HZ9X8B3C4D5F6G7H9J2K1M",
      "platform": "threads",
      "status": "publishing"
    },
    {
      "account_id": "acc_01HZ9X4T6B8C2D5F7G9H1J3K",
      "page_id": "sapi_page_01HZ9X5V7C9D3E6G8H1J4K2M",
      "platform": "facebook",
      "status": "publishing"
    }
  ]
}
```

The post is accepted, and delivery continues after the response. Read the post again or wait for the `post.published`, `post.partial`, or `post.failed` event. [Track post delivery](/guides/delivery) explains the statuses, the HTTP codes, and the per-target errors.

## Choose what the request does

| You send | Result |
| - | - |
| `publish_now: true` and at least one target | The post is published now. Its status is `publishing` |
| `publish_now: true`, at least one target, and `stagger` | The first target publishes now and the others follow at the staggered gaps. Its status is `scheduled`. See [Stagger targets](/guides/scheduling#stagger-targets) |
| `scheduled_at` in the future and at least one target | The post is scheduled. See [Schedule posts](/guides/scheduling) |
| Neither, or no targets | The post is saved as a draft. It is not published, even when you send targets |
| Both `publish_now` and `scheduled_at` | The post is scheduled |

A `scheduled_at` that is not RFC 3339 returns `400 validation.field_invalid`. With at least one target, a `scheduled_at` in the past returns `400 validation.field_invalid`. With no targets, the post is saved as a draft.

## Fields every platform shares

| Field | Type | Description |
| - | - | - |
| `text` | string | The post body. Required, unless every target supplies its own `text` or a `platform_data` field the platform uses as the body (a Pinterest Pin description). A missing body returns `400 validation.field_required` |
| `title` | string | The post title. Used by YouTube (required there) and Zalo (the article title, which falls back to `text`). Other platforms ignore it, including LinkedIn and Google Business Profile. A Pinterest Pin title is `platform_data.pinterest.title` |
| `media` | array | The ordered media list. Each item has `source_type` (`media_id`, `url`, or `platform_attachment_id`), `source`, and `type` for a `platform_attachment_id`. See [Upload media](/guides/media) |
| `visibility` | string | `public`, `private`, `connections_only`, or `logged_in`. Which values apply depends on the platform |
| `first_comment` | string | A comment SocialAPI posts after publishing. Instagram, Facebook, and YouTube read it. Every other platform ignores it, and Facebook Page Stories ignore it |
| `scheduled_at` | string | The publish time (RFC 3339). See [Schedule posts](/guides/scheduling) |
| `publish_now` | boolean | Publish immediately. Default `false` |
| `stagger` | object | Publish the targets one after another with a random gap of `min_seconds` to `max_seconds` between them. Not allowed on a draft. See [Stagger targets](/guides/scheduling#stagger-targets) |
| `skip_validation` | boolean | Publish without the pre-publish validation. Default `false`. The platform can still reject the post |
| `skip_duplicate_check` | boolean | Skip the duplicate-content check. Default `false` |
| `segments` | array | Continuation posts that publish as a thread on X, each with `text`. See [X publishing](/platforms/twitter/publishing) |
| `platform_data` | object | Platform-specific fields, keyed by platform slug. See [Where platform data goes](#where-platform-data-goes) |
| `targets` | array | The destinations. See below |

Each item of `targets` has:

| Field | Description |
| - | - |
| `account_id` | The connected account. Required |
| `page_id` | The `sapi_page_` ID of the Facebook Page when the account has more than one |
| `board_id` | The Pinterest board. Required on a Pinterest target. See [Pinterest boards](/platforms/pinterest/boards) |
| `text`, `title`, `media`, `first_comment`, `visibility`, `scheduled_at` | Override the post-level value for this target. An empty `media` array means this target publishes no media |
| `platform_data` | Platform-specific fields for this target |

A target that does not set a field inherits the post-level value. The same account and Page twice in one request returns `400 publishing.duplicate_target`.

SocialAPI ignores request fields it does not recognize. `POST /v1/posts` is not idempotent: a request returns `409 resource.conflict` when the same account has a post with the same title and text that was created in the last 24 hours and is now `published`. Send `skip_duplicate_check: true` to post the same content again. See [IDs](/guides/ids#create-requests-are-not-idempotent).

## Where platform data goes

Put platform-specific fields in `platform_data`, in one of 2 places:

* **On the post, under the platform slug:** `platform_data.instagram` applies to every Instagram target.
* **On a target, as flat keys:** `targets[].platform_data` applies to that target only.

When both set the same key, the target's value wins. A platform slug used as a key inside a target's `platform_data` is dropped, so write `{"reply_control": "everyone"}` on a target, not `{"threads": {...}}`.

```json theme={null}
{
  "text": "New video is live.",
  "publish_now": true,
  "platform_data": {
    "youtube": {"tags": ["launch"], "category_id": "22"}
  },
  "targets": [
    {"account_id": "acc_01HZ9X9C4D5F6G7H8J1K2M3N", "platform_data": {"made_for_kids": false}}
  ]
}
```

The keys each platform accepts are on its publishing page, for example [YouTube platform data fields](/platforms/youtube/publishing#platform-data-fields).

## Validate before you publish

`POST /v1/posts/validate` checks a post without creating it and costs no credit. Name the platforms with `platforms`, `account_ids`, or `targets`. It accepts the same `text`, `media`, `scheduled_at`, `segments`, and `platform_data` fields as `POST /v1/posts`.

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/posts/validate \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Spring sale starts today: https://example.com/spring",
    "account_ids": ["acc_01HZ9X8B3C4D5F6G7H9J2K1M"]
  }'
```

Response (HTTP 200):

```json theme={null}
{
  "valid": true,
  "errors": [],
  "warnings": []
}
```

`errors` lists the violations that block publishing. `warnings` lists the rest, for example text within 10% of a platform's length limit. Each issue has `platform`, `field`, `message`, and, when it applies to one target, `target` and `segment_index`. A `scheduled_at` that is not in the future returns `400 validation.field_invalid`. Calling it without any platform returns `400 validation.field_required`.

`POST /v1/posts` runs the same validation unless you send `skip_validation: true`. A post with errors returns `400 publishing.validation_failed`, and `error.meta.issues` holds the same list. Drafts skip validation until you publish them.

`GET /v1/posts/validate` returns each platform's text and media rules, so a client can check content before it sends a request. Add `?platform=instagram` for one platform. An unknown platform returns `400 validation.field_invalid`.

```bash theme={null}
curl "https://api.social-api.ai/v1/posts/validate?platform=instagram" \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Response (HTTP 200):

```json theme={null}
{
  "data": {
    "instagram": {
      "text": {"max_length": 2200, "counting_mode": "chars", "max_hashtags": 30, "max_mentions": 20},
      "media": {
        "supported_image_types": ["image/jpeg"],
        "max_image_size_bytes": 8388608,
        "supported_video_types": ["video/mp4", "video/quicktime"],
        "max_video_size_bytes": 314572800,
        "min_video_duration_sec": 3,
        "max_video_duration_sec": 900
      },
      "carousel": {"supported": true, "min_items": 2, "max_items": 10, "allow_video": true, "images_only": false, "photos_only": false},
      "scheduling": {"supported": true, "mode": "server_side"},
      "supports_platform_attachment_id": true,
      "supports_mixed_media": false
    }
  }
}
```

The endpoint covers Instagram, Facebook, Threads, LinkedIn, YouTube, TikTok, Google Business Profile, Bluesky, and X. `counting_mode` is `chars`, `utf16`, or `bytes`. TikTok's per-creator privacy levels and maximum duration come from `GET /v1/accounts/{id}/creator-info`. See [TikTok publishing](/platforms/tiktok/publishing).

`supports_mixed_media` is about `platform_attachment_id` items only: `false` means a `platform_attachment_id` item must be the only item in `media` and cannot sit next to `url` or `media_id` items. It does not limit a carousel, which can mix images and videos where `carousel.allow_video` is `true` and `carousel.images_only` is `false`, as on Instagram.

## Drafts

A post saved without `publish_now` or `scheduled_at` is a `draft`. It costs no credit, skips validation, and can have no targets.

```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": "Draft of the spring announcement."}'
```

The response is HTTP 201 with `"status": "draft"`. Publish it later with `POST /v1/posts/{pid}/publish`, which costs 1 post credit.

## Edit a post before it publishes

`PATCH /v1/posts/{pid}` changes a post that has not been delivered yet. Send only the fields you change.

```bash theme={null}
curl -X PATCH https://api.social-api.ai/v1/posts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Spring sale starts tomorrow: https://example.com/spring"}'
```

The response is HTTP 200 with the updated post. Editing costs no credit.

* **Editable statuses:** `draft`, `scheduled`, and `failed`. Any other status returns `409 post.state_invalid`, and `error.meta.state` is the current status. A `published` post cannot be edited on any platform, except a YouTube video, which has its own endpoint. See [YouTube publishing](/platforms/youtube/publishing).
* **Editable fields:** `text`, `title`, `media`, `visibility`, `first_comment`, `scheduled_at`, `hidden`, `segments`, `platform_data`, and `targets`. Sending `targets` replaces all the post's targets.
* **Validation:** an edit to content runs the validation again. It returns `400 publishing.validation_failed` for a post with errors, unless you send `skip_validation: true`.
* **After a failure:** edit a `failed` post, then call `POST /v1/posts/{pid}/retry`.
* **Event:** SocialAPI sends `post.updated` with `changed_fields`.

## Import posts from a CSV file

`POST /v1/posts/import` creates posts from a CSV file sent as multipart form data in the `file` field. The first row is a header and must start with the `text` column. The file holds at most 500 data rows and 5 MB.

| Column | Description |
| - | - |
| `text` | The post body. Required |
| `title` | The post title |
| `account_ids` | Connected account IDs separated by `\|`. Each ID is one target |
| `scheduled_at` | RFC 3339 time. A row without it is saved as a draft |
| `visibility` | The visibility |
| `first_comment` | The first comment |
| `platform_data` | A JSON object applied to every target of the row |
| `board_id` | The Pinterest board, applied to every target of the row. Give Pinterest its own row |

Columns are read by position, in this order. A row can end early: only `text` is required. There is no `publish_now` column, so an imported post is a draft or a scheduled post.

```bash theme={null}
curl -X POST "https://api.social-api.ai/v1/posts/import?dry_run=true" \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -F "file=@posts.csv"
```

Response (HTTP 200):

```json theme={null}
{
  "total_rows": 3,
  "created": 0,
  "failed": 1,
  "errors": [
    {"row": 3, "message": "invalid date format for scheduled_at"}
  ],
  "posts": []
}
```

`dry_run=true` checks only each row's `text`, `scheduled_at`, and `platform_data` format, and creates nothing. Row numbers count the header as row 1, so the first data row is row 2. A request with no `file` field returns `400 validation.field_required`. A file over 5 MB, a file that is not valid CSV, a header that does not start with `text`, a file with no data rows, or a file with more than 500 data rows returns `400 validation.field_invalid`. When the file has more valid rows than you have post credits left, drafts included, the request returns `429 billing.post_limit` and creates nothing. Each created post is a normal post: it costs 1 post credit when scheduled and nothing as a draft. Without `dry_run`, a row that passes these checks but fails when SocialAPI creates the post (for example `409 resource.conflict` or `400 publishing.validation_failed`) is listed in `errors` with its row number and message and counted in `failed`. The other rows are still created, and the response is still 200: check `created`, `failed`, and `errors`.

## Unpublish and delete

`POST /v1/posts/{pid}/unpublish` removes the published copies from the platforms and keeps the post. It returns 200 `{"success": true}` when every targeted copy was removed, `400 validation.field_invalid` when no target is `published` with a `platform_post_id`, `501 resource.not_supported` when at least one remaining target is on a platform that cannot delete posts (such as TikTok), and `500 system.internal` when a platform fails to delete a copy. With a `501` or `500`, every target that was removed is `unpublished`. See [Unpublish a post](/guides/delivery#unpublish-a-post).

`DELETE /v1/posts/{pid}` deletes a post and also removes its published copies. See [Delete a post](/guides/delivery#delete-a-post).

## Platform support

Each platform accepts different content, sizes, and `platform_data` keys. The cross-platform matrix is on [Platform support](/platforms/overview). The details are on each platform's publishing page:

* [Instagram](/platforms/instagram/publishing)
* [Facebook](/platforms/facebook/publishing)
* [Threads](/platforms/threads/publishing)
* [TikTok](/platforms/tiktok/publishing)
* [YouTube](/platforms/youtube/publishing)
* [LinkedIn](/platforms/linkedin/publishing)
* [X / Twitter](/platforms/twitter/publishing)
* [Pinterest](/platforms/pinterest/publishing)
* [Google Business Profile](/platforms/google/publishing)
* [Zalo](/platforms/zalo/publishing)
* [Bluesky](/platforms/bluesky/publishing)

Every platform accepts `scheduled_at`. A platform that cannot do something returns the error described on its page, for example `501 resource.not_supported`.

## Limits

| Limit | Value |
| - | - |
| Post credits | 1 per published or scheduled post, whatever the number of targets. At the allowance, `POST /v1/posts` returns `429 validation.post_limit_exhausted` |
| Posting speed per account | Per hour and per 24 hours, by platform. At the cap, an immediate publish returns `429 publishing.velocity_limit`. See [Rate limits](/guides/rate-limits#posting-velocity-caps) |
| CSV import | 500 data rows and 5 MB per file. No `file` field returns `400 validation.field_required`. A file over 5 MB, over 500 rows, with a header that does not start with `text`, or with no data rows returns `400 validation.field_invalid` |

## Errors

`400 publishing.validation_failed`, `409 resource.conflict`, `409 post.state_invalid`, and `429 publishing.velocity_limit` are the codes you branch on most. Every code is in [Errors](/guides/errors#publish-a-post). A failure after the request returned appears on the target, not as an HTTP error. A target on an account without the `posts.write` [permission](/guides/permissions) fails with `account.permission_missing`. See [Track post delivery](/guides/delivery#target-errors).

## Scopes

| Scope | Grants |
| - | - |
| `posts:read` | List and read posts |
| `posts:write` | Create, edit, publish, retry, unpublish, validate (`POST /v1/posts/validate`), import, and delete posts |
| `media:write` | Upload and verify the media a post uses |

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

A brand-restricted key can publish only to accounts in its brands. See [Scoped API keys](/guides/scoped-keys).

## Events

| Event | When it fires |
| - | - |
| `post.scheduled` | You create a scheduled post |
| `post.updated` | You edit a post |
| `post.published`, `post.partial`, `post.failed` | The post settles. Each fires once |
| `post.retried` | You retry a post |
| `post.unpublished`, `post.deleted` | You unpublish or delete a post |

Payloads are in [Webhook events](/guides/webhook-events).

## Use from an AI agent

| Tool | What it does |
| - | - |
| `social_api_create_post` | Creates, schedules, or publishes a post |
| `social_api_update_post` | Edits a draft, scheduled, or failed post |
| `social_api_publish_post` | Publishes a draft or scheduled post now |
| `social_api_validate_post` | Checks a post without creating it |
| `social_api_get_constraints` | Returns each platform's rules |

The import endpoint has no tool. See [MCP tools](/integrations/tools).

## Related

* [Track post delivery](/guides/delivery): statuses, HTTP codes, target errors, retry, and delete
* [Schedule posts](/guides/scheduling): publish at a chosen time
* [Upload media](/guides/media): upload files and attach media
* [Platform support](/platforms/overview): what each platform publishes
* [Errors](/guides/errors): every error code with its HTTP status


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