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

# Errors

> Look up every error code the SocialAPI REST API and MCP server return, with its HTTP status, what triggers it, and how to handle it.

Every failed request to the SocialAPI REST API returns a JSON error with an HTTP status, a stable `error.code`, a human-readable `error.message`, and a `request_id`. Branch on `error.code`, never on `error.message`: messages change, codes do not. This page lists every code a client using an API key or the MCP server can receive.

```json theme={null}
{
  "error": {
    "code": "validation.field_required",
    "message": "Required field is missing: platform",
    "meta": { "field": "platform" }
  },
  "request_id": "a1b2c3d4"
}
```

| Field | Description |
| - | - |
| `error.code` | A dot-separated code, stable across releases. Match the whole code, or a prefix such as `platform.` or `billing.` |
| `error.message` | A description for logs and display |
| `error.meta` | Structured context, present on some errors. `meta.field` names the invalid input, `meta.max` is set on `automation.limit_reached` and `meta.plan` on `whatsapp.paid_plan_required`, `meta.retry_after_seconds` is how long to wait |
| `request_id` | Identifies the request. It equals the `X-Request-ID` response header |

`request_id` is the `X-Request-ID` request header you sent when it matches `^[a-zA-Z0-9_-]{1,64}$`. Otherwise it is 8 lowercase hexadecimal characters that SocialAPI generates. Quote it in support requests: see [Debugging](https://docs.social-api.ai/guides/debugging.md).

A `429` response carries `Retry-After` (in seconds) whenever `error.meta.retry_after_seconds` is present.

**MCP server.** A failed tool call returns `isError: true` and a text result holding JSON: `{"error": "<message>", "code": "<error.code>"}`, with the `error.meta` fields added at the top level. The `code` values are the ones on this page. A request refused before the tool runs, such as an expired token, returns an HTTP error: see [Authentication](https://docs.social-api.ai/guides/authentication.md).

## Code namespaces

The first segment of a code tells you which part of the system refused the request.

| Namespace | Meaning |
| - | - |
| `auth.*` | Authentication and authorization |
| `account.*`, `accounts.*` | Connected accounts: lookup, state, Pages, sync |
| `brand.*` | Brand lookup |
| `platform.<name>.*` | The platform (Instagram, TikTok, and the rest) refused the request, or SocialAPI refused it for a platform-specific reason |
| `validation.*` | A request field, parameter, or media item is invalid |
| `resource.*` | Generic lookup, conflict, and capability errors |
| `billing.*` | A plan allowance or plan feature |
| `byok.*` | Your own platform app credentials |
| `publishing.*` | Publishing a post |
| `post.*`, `conversation.*`, `media.*`, `board.*` | Errors on those objects |
| `automation.*` | Automations |
| `whatsapp.*`, `telegram.*`, `google.*` | Errors specific to those platforms |
| `messaging.*`, `inbox.*` | Messaging and sync rate limits |
| `ratelimit.*` | Your SocialAPI account's request budget per minute |
| `system.internal` | The request failed on SocialAPI's side |
| `http.<status>` | A routing or transport error, such as an unknown path |

One code has no namespace: `ambiguous_post_id`.

## Authentication and access

Every `/v1` request needs `Authorization: Bearer <API key>`. See [Authentication](https://docs.social-api.ai/guides/authentication.md) and [Scoped API keys](https://docs.social-api.ai/guides/scoped-keys.md).

| HTTP | Code | When it occurs |
| - | - | - |
| 401 | `auth.missing_header` | The `Authorization` header is absent |
| 401 | `auth.empty_token` | The header is `Authorization: Bearer` with no token |
| 401 | `auth.invalid_key` | The API key does not exist, was revoked, or was rotated away |
| 401 | `auth.invalid_token` | The bearer token is neither a valid API key nor a valid OAuth 2.1 access token, or it has expired |
| 401 | `auth.user_not_found` | The token belongs to a SocialAPI account that no longer exists |
| 403 | `auth.insufficient_scope` | A [scoped API key](https://docs.social-api.ai/guides/scoped-keys.md) lacks the scope the request needs, or a scoped key called an admin operation (managing keys, brands, invites, or `PATCH /v1/users/me`). `meta.required_scope` names the scope when one applies |
| 402 | `billing.past_due` | Your subscription payment failed twice. Every endpoint returns it except every method of `/v1/users/me` and every `/v1/billing/*` endpoint. See [Past-due accounts](https://docs.social-api.ai/guides/plans.md#past-due-accounts) for how access returns |
| 403 | `auth.state_mismatch` | The `state` you passed to a connect step belongs to a different API key |
| 403 | `account.permission_missing` | The connected account does not have the [permission](https://docs.social-api.ai/guides/permissions.md) the call needs. `meta.permission` names it. Add it with `PATCH /v1/accounts/{id}`, or reconnect the account when the platform has not granted it. Publishing, post removal, live post metrics, and the stored inbox lists answer differently: see [Account permissions](https://docs.social-api.ai/guides/permissions.md#what-an-account-can-do-right-now) |
| 400 | `auth.oauth.invalid_client` | The MCP client's `client_id` is unknown. See [OAuth for MCP clients](https://docs.social-api.ai/integrations/oauth.md) |
| 400 | `auth.oauth.invalid_redirect` | The MCP client's `redirect_uri` is not registered for its `client_id` |

The `/oauth/token` endpoint answers errors in the OAuth format (`error`, `error_description`) instead of this envelope: see [OAuth for MCP clients](https://docs.social-api.ai/integrations/oauth.md#token-endpoint-errors).

## Request validation

These codes apply to any endpoint. `error.meta.field` names the offending input when the input is a single field.

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.body_invalid` | The request body is not valid JSON, or has the wrong shape |
| 400 | `validation.field_required` | A required field is missing. `meta.field` names it |
| 400 | `validation.field_invalid` | A field or parameter has the wrong type, format, or value. `meta.field` names it. Examples: `from` is not RFC 3339, `cursor` is malformed, a webhook `url` is not an `https` URL, a webhook event type is unknown, a media `url` is not a public `http` or `https` address, `scheduled_at` is in the past on a post with targets, `stagger` is sent on a draft, or `stagger` is outside `0 <= min_seconds <= max_seconds <= 86400` |
| 400 | `validation.id_invalid` | A comment or review ID in the path is neither a platform ID nor a SocialAPI ID |
| 400 | `validation.id_platform_mismatch` | A comment or review ID belongs to a different platform than the `account_id` |

## Not found, conflicts, and unsupported operations

| HTTP | Code | When it occurs |
| - | - | - |
| 404 | `resource.not_found` | The object does not exist or is not visible to your key. `meta.resource` names the type (for example `post`, `automation`, `brand`) and `meta.id` the ID |
| 404 | `account.not_found` | The `acc_` ID is not a connected account of your SocialAPI account |
| 404 | `brand.not_found` | The brand is outside the brands your [brand-restricted key](https://docs.social-api.ai/guides/scoped-keys.md) can reach |
| 404 | `conversation.not_found` | The conversation does not exist or is not yours |
| 404 | `media.not_found` | The media item does not exist or is not yours |
| 409 | `resource.conflict` | The request conflicts with current state. Duplicate post content and invite creation use it: see below |
| 409 | `ambiguous_post_id` | The platform post ID you passed matches more than one post. Use the SocialAPI post ID |
| 501 | `resource.not_supported` | The operation does not exist on this account's platform. `meta.platform` names it. See the [capability matrix](https://docs.social-api.ai/platforms/overview.md) |
| 422 | `account.platform_unsupported` | The account's platform has no reviews, and the request was for reviews |
| 4xx, 5xx | `http.<status>` | A routing or transport error. `http.404` is an unknown path. `GET /v1/inbox/media/{id}` also returns `http.404` (unknown media), `http.410` (media expired), `http.503` (too many waiting requests, retry after 1 second), and `http.504` (media still being fetched, send the request again until you get 200 or 410) |

## Rate limits

Rate-limit codes are `429` and are covered in [Rate limits](https://docs.social-api.ai/guides/rate-limits.md), which says what each limit counts and how to retry.

| HTTP | Code | When it occurs |
| - | - | - |
| 429 | `ratelimit.exceeded` | Your SocialAPI account used its request budget for the current minute. `meta.limit` is the budget and `meta.retry_after_seconds` the wait. See [Request rate limit](https://docs.social-api.ai/guides/rate-limits.md#request-rate-limit) |
| 429 | `publishing.velocity_limit` | A publish target reached its post cap per hour or per 24 hours. `meta.window` is `hour` or `day` |
| 429 | `platform.<name>.rate_limit` | The platform's own limit. Wait, then retry with backoff. Credit rules: see [when a credit comes back](https://docs.social-api.ai/guides/plans.md#when-a-credit-comes-back) |
| 429 | `inbox.sync_rate_limited` | `POST /v1/inbox/sync` ran again before the wait for the same target ended. `meta.retry_after_seconds` is set |
| 429 | `accounts.sync_rate_limited` | `POST /v1/accounts/{id}/sync` ran again before the wait for the same account ended. `meta.retry_after_seconds` is set |

## Billing and plan allowances

Allowances and billing periods are defined in [Plans and usage](https://docs.social-api.ai/guides/plans.md).

| HTTP | Code | When it occurs |
| - | - | - |
| 429 | `validation.post_limit_exhausted` | The post allowance for the billing period is used up. It resets at the start of the next period |
| 429 | `validation.interaction_limit_exhausted` | The interaction allowance for the billing period is used up |
| 429 | `billing.post_limit` | `POST /v1/posts/import` holds more posts than the allowance has credits left. `error.message` gives the credits needed and remaining |
| 403 | `billing.brands_limit` | Creating the brand would exceed your plan's brand allowance |
| 403 | `billing.platform_not_available` | The platform slug is not one of the connectable platform slugs, such as `trustpilot` |
| 413 | `billing.storage_quota` | The upload would exceed your plan's media storage |
| 429 | `billing.export_limit` | You reached your plan's number of exports per billing period |
| 429 | `billing.export_cooldown` | The account was exported within your plan's cooldown period |
| 429 | `billing.export_concurrent` | Another export is still pending or processing |
| 403 | `whatsapp.paid_plan_required` | Sending a WhatsApp template needs a paid plan. Replies to people who messaged you do not |
| 402 | `billing.past_due` | See [Authentication and access](#authentication-and-access) |

## Connect an account

These codes come from `POST /v1/accounts/connect`, the steps that follow the consent screen, and the Page and location pickers. See [Connect accounts](https://docs.social-api.ai/guides/connect-accounts.md) for the flow.

A code marked **Redirect** has no HTTP status: it reaches your `redirect_uri` as `status=error` and `error=<code>`, with `error_description`.

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.connection_type_invalid` | `metadata.connection_type` on a TikTok connect is present but is not the string `business` or `creator`. `meta.field` is `connection_type` |
| 400 | `validation.customer_state_too_long` | `state` is longer than 512 characters |
| 400 | `validation.permission_unknown` | A value in `permissions` is not a [permission](https://docs.social-api.ai/guides/permissions.md#the-permissions). `meta.permission` names it. `POST /v1/accounts/connect`, `POST /v1/invites`, and `PATCH /v1/accounts/{id}` return it |
| 400 | `validation.permissions_empty` | `permissions` is an empty list. Send at least one permission, or omit the field. `PATCH /v1/accounts/{id}` also returns it, where `null` gives the account every permission |
| 400 | `validation.redirect_uri_invalid_format` | `redirect_uri` is malformed |
| 400 | `validation.redirect_uri_not_whitelisted` | `redirect_uri` is not registered. Register it in the dashboard first |
| 400 | `validation.state_invalid` | A connection `state` is unknown or expired |
| 400 | `validation.profile_not_in_set` | The `platform_account_id` you selected is not one of the candidates for this connection |
| 400 | `validation.page_not_in_set` | A Page ID in `page_ids` is not one of the candidates for this connection. `meta.platform_page_id` names it |
| 400 | `validation.page_already_assigned` | The Facebook Page already belongs to another brand. `meta.brand_id` names that brand |
| 403 | `platform.facebook.page_cap` | The selection or assignment would give the brand more Facebook Pages than your plan allows per brand. `meta.limit` is the allowance. See [Facebook Pages per brand](https://docs.social-api.ai/guides/plans.md#facebook-pages-per-brand) |
| 409 | `platform.instagram.flow_conflict` | You hold the Instagram account through the other login path: you selected it with Facebook Login while you hold it through Instagram Login, or the reverse. `meta.accounts` is a comma-separated string of the Instagram account IDs. Disconnect the account first. On Instagram Login it reaches your `redirect_uri` as `error=platform.instagram.flow_conflict` |
| 400 | `validation.page_not_granted` | The Facebook Page is not accessible with this login |
| 400 | `validation.location_not_granted` | The Google location is not accessible with this login |
| 400 | `validation.location_already_assigned` | The Google location already belongs to another brand |
| 400 | `validation.invalid_account_id` | The Google account ID has an invalid format |
| 400 | `validation.invalid_location_id` | The Google location ID is not valid |
| 400 | `validation.auth_code_expired` | The TikTok authorization code expired or was already used. Start the connection again |
| 409 | `account.already_linked` | The brand already holds a different account on this platform, or `lock_accounts_to_brand` is `true` and the account belongs to another of your brands (see [Reconnect an account](https://docs.social-api.ai/guides/connect-accounts.md#reconnect-an-account)). In the 409 response, `meta.brand_id` is the brand you named, or with the lock, the brand that holds the account. Telegram, Bluesky, and choosing a Google location with `POST /v1/accounts/pending/{connection_id}/select` return the 409. On a platform with a consent screen it reaches your `redirect_uri` as `error=account.already_linked` |
| 409 | `account.different_login` | The brand's Facebook, or its Instagram connected through Facebook Login, is connected through a different login. Disconnect it first. During a connection it reaches your `redirect_uri` as `error=account.different_login`; assigning a Page returns the 409 |
| 403 | `account.reconnection_required` | The platform token is no longer usable. Reconnect with `POST /v1/accounts/connect`, passing the existing connected account's brand and platform: see [Reconnect an account](https://docs.social-api.ai/guides/connect-accounts.md#reconnect-an-account) |
| 400 | `account.unsupported_platform` | The platform value is not recognized |
| Redirect | `platform.facebook.no_pages` | The Facebook login has no Pages |
| Redirect | `platform.instagram.no_pages` | The Facebook login has no Instagram account: see [Connect through Facebook Login](https://docs.social-api.ai/platforms/instagram#connect-through-facebook-login) |
| Redirect | `platform.google.no_locations` | The Google login has no Business Profile locations |
| Redirect | `platform.youtube.no_channels` | The Google login has no YouTube channels |
| 404 | `platform.youtube.no_uploads_playlist` | The YouTube channel has no uploads playlist |
| Redirect | `platform.facebook.no_assignable_pages` | On a connection without a [selection step](https://docs.social-api.ai/guides/connect-accounts.md#facebook-pages), every Page on the login already belongs to another brand |
| Redirect | `platform.instagram.no_assignable_pages` | On a connection without a selection step, every Instagram account on the Facebook login already belongs to another brand, or you hold it through Instagram Login |
| 409 | `platform.google.brand_has_account` | Assigning a location when the brand already has a Google Business Profile connected. Disconnect it first, or choose another brand |
| 502 | `platform.instagram.api_error` | Instagram's token exchange failed during connection |
| 400 | `account.page_not_found` | The `page_id` matches no Page of the account |
| 400 | `account.no_active_page` | The account has no active default Page |
| 400 | `account.page_inactive` | The Page is deactivated |
| 400 | `account.pages_not_supported` | The platform has no Pages |
| 400 | `account.use_boards_endpoint` | A Pinterest account has boards, not Pages. Use `/v1/accounts/{id}/boards` |
| 400 | `account.boards_not_supported` | The account is not Pinterest, which is the only platform with boards |
| 403 | `whatsapp.number_banned` | Meta banned the WhatsApp number, so it cannot be connected |
| 400 | `whatsapp.ambiguous_phone_number` | The WhatsApp Business account has more than one phone number. Pass `phone_number_id` |
| 400 | `whatsapp.no_phone_number` | The WhatsApp Business account has no phone number. Add one, then connect again |
| 409 | `whatsapp.business_app_number` | The number is still in the WhatsApp Business app and is already registered. Do not call register for it |
| 400 | `platform.bluesky.auth_factor_required` | The Bluesky account has email two-factor authentication on. Retry with the emailed code as `metadata.auth_factor_token` |

## BYOK app credentials

Platforms that use [your own app](https://docs.social-api.ai/guides/platform-credentials.md) (X) return these codes.

| HTTP | Code | When it occurs |
| - | - | - |
| 412 | `byok.credentials_missing` | You saved no app credentials for the platform. `POST /v1/accounts/connect` returns it |
| 401 | `byok.credentials_invalid` | The platform rejected the saved app credentials. When you connect an account, it arrives as the `error` parameter on your redirect URI |
| 412 | `byok.app_access_denied` | The platform's app tier does not allow the call. Upgrade the app's access on the platform. When you connect an account, it arrives as the `error` parameter on your redirect URI |

## Brands, invites, and keys

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.field_required` | `name` is missing when creating a brand or a key, or `brand_id` is missing when creating an invite |
| 400 | `validation.field_invalid` | `expires_in_days` is not one of 1, 3, 7, 14, or 30 (including when it is omitted) |
| 409 | `resource.conflict` | `POST /v1/invites` found an active invite for the brand and platform, or a connected account for that platform on the brand. Only `error.message` tells the two apart |
| 400 | `validation.scope_invalid` | A key was created or edited with an unknown scope. The message names it |
| 400 | `validation.brand_invalid` | A key's `brand_ids` names a brand that does not exist or that you do not own |
| 403 | `billing.brands_limit` | Creating the brand would exceed your plan's allowance |

An empty `GET /v1/invites` list does not mean `POST /v1/invites` succeeds: the brand can already hold a connected account for the platform. Check `GET /v1/accounts` as well.

## Publish a post

These codes come from `POST /v1/posts`, `POST /v1/posts/validate`, the post lifecycle endpoints, and the post media fields. Platform-specific media rules are under [Platform-specific codes](#platform-specific-codes). See [Publishing](https://docs.social-api.ai/guides/publishing.md) and [Delivery](https://docs.social-api.ai/guides/delivery.md).

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `publishing.validation_failed` | The post failed validation. `error.meta.issues` lists each issue. Fix them, or send `skip_validation` |
| 400 | `publishing.no_targets` | The post has no publishable targets |
| 400 | `publishing.duplicate_target` | One account appears twice in the request with the same Page |
| 400 | `validation.field_required` | `text` or another required post field is missing |
| 400 | `validation.board_id_required` | A Pinterest target has no `board_id` |
| 400 | `validation.duplicate_board_id` | A Pinterest target sets both `board_id` and `page_id`. Use `board_id` |
| 400 | `validation.use_board_id` | A Pinterest board was set in `platform_data.board_id`. Set `board_id` on the target |
| 400 | `validation.use_page_id` | `board_id` was set on a non-Pinterest target. Use `page_id` |
| 409 | `resource.conflict` | 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 publish the content again |
| 409 | `post.state_invalid` | The post's status does not allow the call. Edit works on `draft`, `scheduled`, or `failed`. Publish works on `draft` or `scheduled`. Retry works on `failed` or `partial`. Delete does not work on a `publishing` post. `meta.state` is the current status |
| 400 | `post.no_retryable_deliveries` | The post has no failed targets to retry |
| 422 | `publishing.media.missing_source` | A media item has no source |
| 422 | `publishing.media.invalid_source_type` | `source_type` is not `url`, `media_id`, or `platform_attachment_id` |
| 422 | `publishing.media.missing_type` | A `platform_attachment_id` item has no `type` (`image`, `video`, `audio`, or `file`) |
| 422 | `publishing.media.untyped_url` | The media type cannot be read from the `url`. Use a URL with an extension such as `.jpg` or `.mp4`, or upload the file |
| 422 | `publishing.media.unsupported_source` | The platform does not accept `platform_attachment_id` media. Only Instagram, Threads, LinkedIn, and X do |
| 422 | `publishing.media.mixing_unsupported` | Instagram or Threads cannot combine a `platform_attachment_id` with other media. It must be the only item |
| 500 | `publishing.media.proxy_unavailable` | SocialAPI could not prepare the media for the platform. Retry the request |
| 429 | `publishing.velocity_limit` | See [Rate limits](#rate-limits) |

`POST /v1/posts` can also return the `billing.*` and `validation.post_limit_exhausted` codes in [Billing and plan allowances](#billing-and-plan-allowances). Publishing failures that happen after the request returns appear on the post: see [Delivery failures](#delivery-failures).

## Delivery failures

A target that fails after `POST /v1/posts` returned is a failed delivery. It does not return an HTTP error. Its code is in `targets[].error.code` on the post (`GET /v1/posts/{pid}`) and in `results[].error.code` of the `post.failed` and `post.partial` webhooks. `GET /v1/events` has no error code. See [Delivery](https://docs.social-api.ai/guides/delivery.md#target-errors).

| Code | When it occurs |
| - | - |
| `publishing.delivery.account_load_failed` | SocialAPI could not load the connected account when the delivery ran |
| `account.permission_missing` | The account did not have the `posts.write` [permission](https://docs.social-api.ai/guides/permissions.md) when the delivery ran |
| `publishing.delivery.page_resolve_failed` | SocialAPI could not resolve the target's Facebook Page |
| `publishing.delivery.timeout` | Sending did not finish within 15 minutes |
| `validation.publish` | The post failed validation when the delivery ran, after the request had been accepted |
| `platform.<name>.publish_failed` | The platform failed the publish and gave no code SocialAPI maps |
| `platform.<name>.<code>` | Any [platform error](#platform-errors) the platform raised during the publish |
| `system.internal` | The target never finished, or a SocialAPI error stopped it |

## Inbox: comments, messages, and reviews

See [Comments](https://docs.social-api.ai/guides/comments.md), [Direct messages](https://docs.social-api.ai/guides/direct-messages.md), [Reviews](https://docs.social-api.ai/guides/reviews.md), and [Mentions](https://docs.social-api.ai/guides/mentions.md). Messages and replies spend interaction credits, so they can also return `validation.interaction_limit_exhausted`.

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `platform.instagram.window_closed`, `platform.facebook.window_closed` | The 24-hour messaging window is closed. The person must message you first |
| 400 | `platform.instagram.private_reply_ineligible`, `platform.facebook.private_reply_ineligible` | The comment cannot receive a private reply: it is older than 7 days, or already received one |
| 422 | `platform.instagram.attachment_failed`, `platform.facebook.attachment_failed` | The platform could not fetch or accept an attachment on a DM or a private reply. Check that the URL is publicly reachable and the file type and size are supported, then send again. A private reply refused this way leaves the comment eligible |
| 422 | `platform.instagram.thread_unavailable`, `platform.facebook.thread_unavailable` | The platform refused to deliver to this conversation. It can be restricted or unavailable |
| 409 | `platform.instagram.no_page` | A DM or private reply on a Facebook Login account with no Facebook Page linked to it. Reconnect the account |
| 422 | `platform.instagram.invalid_parameter` | Instagram rejected a parameter. Retrying unchanged will not help |
| 403 | `platform.instagram.permission_denied`, `platform.facebook.permission_denied`, `platform.whatsapp.permission_denied` | The scopes granted to the connected account do not cover the action |
| 400 | `platform.whatsapp.invalid_parameter` | WhatsApp rejected a parameter. `error.message` carries WhatsApp's reason |
| 400 | `validation.field_required` | The message has no `text`, attachment, or interactive content. A review ID or a comment path parameter is missing |
| 400 | `validation.field_invalid` | `status` is not `active` or `archived`, `moderation_status` is not an allowed value, or `order` is not `newest`, `rating_asc`, or `rating_desc` |
| 400 | `validation.attachment_invalid` | An attachment has no `source_type` |
| 400 | `validation.attachment_type_required` | An attachment of source `platform_attachment_id` has no `type` |
| 400 | `validation.media_unresolved` | A `media_id` attachment could not be resolved |
| 400 | `validation.invalid_message_tag` | The `message_tag` is not supported. Instagram Login accepts only `HUMAN_AGENT` |
| 400 | `validation.message_required` | `message_id` is missing when reacting to a message |
| 400 | `validation.participant_required` | `participant_id` is missing when reacting to a message, or the conversation's X participant is unknown |
| 400 | `validation.invalid_reaction` | The reaction is not a single emoji character |
| 400 | `validation.interactive_conflict` | More than one of `quick_replies`, `buttons`, and `cards` (Instagram), or of `buttons` and `list_sections` (WhatsApp), was sent |
| 400 | `validation.too_many_quick_replies`, `validation.too_many_buttons`, `validation.too_many_cards`, `validation.too_many_sections`, `validation.too_many_rows` | An interactive message exceeds a count limit. Instagram: 13 quick replies, 3 buttons per card, 10 cards. WhatsApp: 3 buttons, 10 sections, 10 rows across all sections |
| 400 | `validation.invalid_button_count` | A button template has more than 3 buttons: on an Instagram DM, or on an Instagram or Facebook private reply |
| 400 | `validation.invalid_button_title`, `validation.title_too_long`, `validation.quick_reply_title_too_long`, `validation.card_text_too_long`, `validation.description_too_long`, `validation.text_too_long` | A title or text is empty or too long. Limits: button title 20 characters, quick reply title 20, card title 80, card subtitle 80, button template text 640 (Instagram DMs, and private replies on Instagram and Facebook), WhatsApp interactive text 1,024, WhatsApp `list_button` 20, WhatsApp section title 24, WhatsApp row title 24, WhatsApp row description 72 |
| 400 | `validation.invalid_button_type` | A button `type` is not `url` or `postback` |
| 400 | `validation.button_type_unsupported` | A WhatsApp interactive button is not `postback`. URL buttons exist only in templates |
| 400 | `validation.cards_unsupported`, `validation.quick_replies_unsupported` | WhatsApp has no carousel or quick replies. Use buttons or `list_sections` |
| 400 | `validation.duplicate_payload` | WhatsApp button payloads are not unique |
| 400 | `validation.attachment_unsupported` | X direct messages cannot carry attachments through the API. Send text only |
| 400 | `validation.text_required` | X requires message text |
| 403 | `platform.bluesky.not_own_record` | Bluesky lets you delete only your own replies. Hide the reply instead |
| 403 | `platform.bluesky.not_thread_owner` | Only the author of the original Bluesky post can hide replies to it |
| 422 | `platform.bluesky.hidden_replies_full` | The Bluesky thread already holds 300 hidden replies |
| 409 | `platform.bluesky.stale_record` | The Bluesky record changed during the request. Retry |
| 404 | `resource.not_found`, `platform.<name>.<code>` | The comment's post was deleted on the platform. Later requests for the same post return the same 404 |

WhatsApp messaging errors are in [WhatsApp](#whatsapp). Telegram's are in [Telegram](#telegram).

## Automations

Returned by `POST /v1/automations` and `PATCH /v1/automations/{id}`. The rules behind each code are in [Automations](https://docs.social-api.ai/guides/automations.md).

| HTTP | Code | When it occurs |
| - | - | - |
| 409 | `automation.limit_reached` | The account already has 50 Automations. `error.meta.max` is `50` |
| 422 | `automation.platform_unsupported` | The account is not an Instagram or Facebook account |
| 404 | `resource.not_found` | No Automation with this ID is visible to your key (`error.meta.resource` is `automation`) |
| 404 | `account.not_found` | `account_id` is not an active account your key can reach |
| 403 | `auth.insufficient_scope` | The key lacks `automations:write`, or the scope one of the actions needs (`inbox:write` or `dms:send`). `meta.required_scope` names it |

A definition that breaks a rule returns `400` with one of these codes, and `error.meta.field` points at the offending field (for example `steps[1].buttons`).

| Code | When it occurs |
| - | - |
| `validation.field_required` | `account_id` is missing on create |
| `validation.name_invalid` | `name` is empty or longer than 100 characters |
| `validation.status_invalid` | `status` is not `active` or `paused` |
| `validation.trigger_invalid` | `trigger` is not `comment` or `dm` |
| `validation.post_ids_invalid` | `post_ids` on a `dm` Automation, more than 100 IDs, or an ID that is empty or over 100 characters |
| `validation.steps_invalid` | Fewer than 1 or more than 10 steps |
| `validation.step_key_invalid` | A step key is duplicated or not 1 to 32 characters of `a-z`, `0-9` and `_`, or no step is keyed `start` |
| `validation.step_ref_invalid` | A `next_step` or `fallthrough_step` names a step that does not exist, or a button on a comment Automation leads back to `start` |
| `validation.fallthrough_invalid` | `start` has a `fallthrough_step` |
| `validation.fallthrough_cycle` | `fallthrough_step` links form a cycle |
| `validation.filter_invalid` | Unknown filter type, more than 10 filters on a step, or `follows_account` on `start` or with a `keyword` |
| `validation.filter_unsupported` | `follows_account` (or a `follow_gate`) on a Facebook account |
| `validation.keywords_invalid` | A keyword filter has no keywords or more than 50, or a keyword is empty or over 100 characters |
| `validation.match_invalid` | `match` is not `contains` or `exact` |
| `validation.actions_invalid` | A step has no actions |
| `validation.action_invalid` | Unknown action type |
| `validation.action_not_allowed` | The action is not available on that step: steps after `start` accept only `send_dm`, and each trigger has its own allowed actions |
| `validation.action_duplicate` | The same action type appears twice on a step |
| `validation.texts_invalid` | An action has no texts or more than 10, a text is empty or over 1,000 characters, or over 640 characters on a `private_reply` or `send_dm` whose step has buttons |
| `validation.buttons_invalid` | More than 3 buttons, buttons on a step without a `private_reply` or `send_dm`, a title that is missing or over 20 characters, a `url` button without an `http` or `https` URL, a `postback` button without `next_step`, a button carrying the other type's field, or an unknown button type |
| `validation.permission_missing` | The trigger or an action needs a [permission](https://docs.social-api.ai/guides/permissions.md#automations) the account does not have. `error.meta.permission` names it |
| `validation.follow_gate_invalid` | `follow_gate` sent together with `steps`, or with the `dm` trigger |

An action that fails when an Automation runs is not returned as an error. It is recorded on the run with the same code the API call would have returned: see [Firings](https://docs.social-api.ai/guides/automations.md#firings).

## Webhooks

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.field_required` | `url` is missing |
| 400 | `validation.field_invalid` | `url` is not a valid `https` URL, or an event type in `events` is unknown |
| 400 | `validation.body_invalid` | A `PATCH` set no fields |
| 400 | `validation.webhook_verification_failed` | `POST /v1/webhooks/{id}/test` could not build a request to the endpoint URL |

## Media library and exports

| HTTP | Code | When it occurs |
| - | - | - |
| 413 | `billing.storage_quota` | The upload would exceed your plan's storage. Check `GET /v1/media/storage`, delete unused media, or change plan |
| 400 | `account.exports_not_supported` | The account is a Pinterest account. Pinterest data is read live and cannot be exported |
| 400 | `account.export_enrichment_not_supported` | `include_transcript` or `include_vision` on a TikTok account on the default connection, whose exports hold captions and metrics only. Export again without those options. A TikTok [creator connection](https://docs.social-api.ai/platforms/tiktok#creator-connection) accepts both |
| 400 | `validation.field_invalid` | The export is not completed yet, so its videos cannot be read |
| 429 | `billing.export_limit`, `billing.export_cooldown`, `billing.export_concurrent` | See [Billing and plan allowances](#billing-and-plan-allowances) |

## Analytics and insights

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.from_invalid`, `validation.to_invalid` | `from` or `to` is not formatted `YYYY-MM-DD` |
| 400 | `validation.range_inverted` | The end of a date or time range is before its start |
| 400 | `validation.range_too_wide` | The analytics range is longer than 400 days |
| 403 | `account.permission_missing` | The account does not have the `analytics.read` [permission](https://docs.social-api.ai/guides/permissions.md). Account analytics and the platform insight endpoints return it, and so does post analytics when no target of the post has the permission |
| 403 | `platform.instagram.insights_scope_missing` | Instagram withdrew the insights access. Reconnect the account |
| 403 | `platform.threads.insights_scope_missing` | The account was connected before 2026-10-17 or without the `threads_manage_insights` scope. Reconnect the account |
| 403 | `platform.facebook.insights_scope_missing` | The Page was connected before 2026-10-17, its login declined the `read_insights` scope, or the person who connected it does not hold the Analyze task on the Page. A reconnect fixes the first two; for the third, grant the Analyze task on Facebook, then reconnect |
| 422 | `platform.instagram.insights_unavailable` | Instagram has no insights of the requested kind for this media |
| 400 | `validation.days_invalid` | Facebook Page insights `days` is below 1 or above 365 |
| 400 | `validation.metric_required` | No metric was passed to a Threads or Google Business Profile insights request |
| 400 | `validation.metric_invalid` | A metric is not valid for the request (and, on Threads, for the `level`) |
| 400 | `validation.level_invalid` | Threads insights `level` is not `media` or `account` |
| 400 | `validation.post_id_required` | Threads insights at `level=media` has no `post_id` |
| 400 | `validation.range_not_supported` | `since` or `until` was sent at Threads `level=media`, which returns lifetime totals |
| 400 | `validation.since_invalid`, `validation.until_invalid` | Threads `since` or `until` is not a Unix timestamp in seconds |
| 400 | `validation.since_too_early`, `validation.until_too_early` | Threads `since` or `until` is before 2024-04-13 |
| 400 | `validation.breakdown_required`, `validation.breakdown_unknown`, `validation.breakdown_multiple`, `validation.breakdown_not_applicable`, `validation.breakdown_range_unsupported` | A Threads `follower_demographics` request has a missing, unknown, repeated, misplaced breakdown, or sends `since` or `until`. The breakdowns are `country`, `city`, `age`, and `gender` |
| 400 | `validation.start_date_required`, `validation.start_date_invalid`, `validation.end_date_required`, `validation.end_date_invalid` | A Google insights date is missing or not formatted `YYYY-MM-DD` |
| 400 | `validation.start_month_required`, `validation.start_month_invalid`, `validation.end_month_required`, `validation.end_month_invalid` | A Google search keywords month is missing or not formatted `YYYY-MM` |
| 400 | `validation.page_size_invalid` | Google search keywords `limit` is negative or above 100 |
| 400 | `validation.username_required` | `username` is missing from a Threads profile lookup |

## Google Business Profile

| HTTP | Code | When it occurs |
| - | - | - |
| 422 | `google.media.single_item_required` | A media upload carried other than exactly one media item |
| 422 | `google.media.invalid_category` | The media `category` is not an allowed value |
| 422 | `google.media.unresolvable` | The media item could not be resolved |
| 400 | `validation.invalid_media_category` | The media `category` is not an allowed value |
| 400 | `validation.invalid_media_source` | The media source is not a `url`, a stored media ID, or a Google upload reference |
| 400 | `validation.missing_media_source` | A media source is required |
| 400 | `validation.media_not_in_account` | The media item does not belong to this location |
| 400 | `validation.media_not_updatable` | A Google post's media cannot change after publishing. Delete the post and create a new one |
| 400 | `validation.invalid_call_to_action` | The call to action is not valid. `error.message` gives the reason |
| 400 | `validation.no_fields_to_update` | A location update set no updatable field |
| 503 | `platform.google.service_disabled` | Google's API is temporarily unavailable. Retry the request |
| 403 | `platform.google.access_denied` | Google denied access to the location |

## Platform-specific codes

### Instagram and Facebook

| HTTP | Code | When it occurs |
| - | - | - |
| 422 | `platform.instagram.container_failed` | Instagram could not process the media. Check the file against Instagram's specifications |
| 422 | `platform.instagram.container_expired` | The media container expired before publishing. Create the post again |
| 504 | `platform.instagram.container_timeout` | The media was still processing after about 5 minutes |
| 503 | `platform.instagram.1`, `platform.instagram.2` | Instagram reported a temporary failure. Retry the request |
| 403 | `platform.instagram.200` | Instagram says the connected account has not granted a scope the call needs. Reconnect the account to grant it |
| 404 | `platform.instagram.100` | Instagram says the object does not exist, or does not accept the request. Check the ID |
| 404 | `platform.instagram.9010` | Instagram has no account for the participant ID, for example because the person deleted or deactivated the account |
| 409 | `platform.instagram.9007` | Instagram is not ready to publish the media yet. Retry the publish |
| 403 | `platform.instagram.230` | Instagram withholds the participant profile: the person has not messaged the account or tapped an icebreaker or the persistent menu |
| 404 | `platform.facebook.100` | Facebook says the object does not exist, or the Page lacks a scope the call needs. Check the ID and the Page scopes |
| 403 | `platform.facebook.230` | Facebook requires the user's consent to access the resource. Reconnect the account |
| 504 | `platform.facebook.reel_timeout`, `platform.facebook.story_timeout` | Facebook did not finish processing the Reel or Story video within the time allowed |
| 422 | `platform.facebook.reel_processing_failed`, `platform.facebook.story_processing_failed` | Facebook could not process the Reel or Story video |
| 400 | `validation.media_invalid` | A Facebook Reel does not have exactly one video, and the post was sent with `skip_validation: true`. Without it, the post returns `400 publishing.validation_failed` |
| 400 | `validation.media_mixed` | A Facebook multi-photo post includes a video, and the post was sent with `skip_validation: true`. Without it, the post returns `400 publishing.validation_failed` |
| 400 | `validation.media_too_many` | A Facebook multi-photo post has more than 10 images, and the post was sent with `skip_validation: true`. Without it, the post returns `400 publishing.validation_failed` |

### Threads

| HTTP | Code | When it occurs |
| - | - | - |
| 422 | `platform.threads.THREADS_API__LINK_LIMIT_EXCEEDED` | The post holds more than 5 unique links |
| 422 | `platform.threads.ERROR`, `platform.threads.EXPIRED` | Threads rejected the media, or the media expired before publishing |
| 504 | `platform.threads.container_timeout` | Threads did not finish processing the media after about 4 minutes |
| 422 | `platform.threads.801` | `follower_demographics` was requested for an account with fewer than 100 followers |
| 429 | `platform.threads.rate_limit` | Threads' own rate limit. Credit rules: see [when a credit comes back](https://docs.social-api.ai/guides/plans.md#when-a-credit-comes-back) |
| 403 | `platform.threads.<code>` | A call needs a Threads scope that the account lacks, such as hiding a reply without `threads_manage_replies`. `<code>` is Meta's error code |
| 404 | `resource.not_found` | The post, reply, or profile does not exist, or a profile is not discoverable |

### TikTok

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.media_required` | A video post has no media URL |
| 400 | `validation.media_count` | A photo post does not hold 2 to 35 images |
| 400 | `validation.media_type_mismatch` | An image was sent as a video. Set `media_type` to `photo` and attach 2 or more JPEG or WebP images |
| 400 | `validation.unsupported_photo_format` | A photo post includes an image that is not JPEG or WebP |
| 400 | `validation.unsupported_media_type` | `media_type` is not `video` or `photo` |
| 400 | `validation.post_mode` | `post_mode` is neither `direct` nor `inbox` |
| 400 | `validation.privacy_level_required` | A photo carousel has no `privacy_level`. On a TikTok [creator connection](https://docs.social-api.ai/platforms/tiktok#creator-connection), a published video or photo carousel (not a draft) has none |
| 400 | `validation.posting_limit` | A TikTok [creator connection](https://docs.social-api.ai/platforms/tiktok#creator-connection) has no privacy level TikTok allows right now, so it cannot post. `can_post` is `false` in [Creator info](https://docs.social-api.ai/platforms/tiktok#creator-info) |
| 400 | `validation.privacy_level_unsupported` | A video post sets a `privacy_level` other than `PUBLIC_TO_EVERYONE`, or a `privacy_level` that is not a string. `meta.privacy_level` is the value you sent |
| 400 | `validation.invalid_privacy_level` | A photo carousel sets a `privacy_level` the account does not allow, or, on a TikTok creator connection, a video or a photo carousel does. `meta.privacy_level` is the value you sent |
| 400 | `validation.branded_content_private` | A photo carousel sets `brand_content_toggle: true` with `privacy_level: "SELF_ONLY"`, or, on a TikTok creator connection, a video or a photo carousel does |
| 400 | `validation.comment_id_invalid` | A comment ID is a bare TikTok comment ID. Hide, unhide, reply, and list replies need the `id` or `platform_id` returned by the comments endpoint. `meta.comment_id` is the value you sent |
| 400 | `validation.mention_reply_unsupported` | A reply to a TikTok mention names no comment. TikTok replies only to mentions made in a comment |
| 400 | `platform.tiktok.40000`, `platform.tiktok.40002`, `platform.tiktok.40011`, `platform.tiktok.40904`, `platform.tiktok.40905`, `platform.tiktok.40906`, `platform.tiktok.40907`, `platform.tiktok.40908`, `platform.tiktok.40914`, `platform.tiktok.40915` | TikTok refused a parameter or a media file. `error.message` carries TikTok's reason. Fix the field it names |
| 403 | `platform.tiktok.40001`, `platform.tiktok.40118`, `platform.tiktok.40125` | The account's authorization lacks a scope the call needs. Reconnect the account to grant it |
| 422 | `platform.tiktok.publish_failed` | TikTok rejected the post while processing it, or could not report its outcome. `error.message` carries TikTok's reason. Check the account in the TikTok app before retrying |
| 401 | `platform.tiktok.auth` | TikTok's authorization for the account ended. Later calls return `403 account.reconnection_required` until you reconnect |
| 401 | `platform.tiktok.unauthorized` | TikTok refused the call as unauthorized without saying the authorization ended. The account stays usable. Reconnect if it repeats |
| 429 | `platform.tiktok.rate_limit` | TikTok's own request limit of 40 requests per minute for each account and each TikTok call. Credit rules: see [when a credit comes back](https://docs.social-api.ai/guides/plans.md#when-a-credit-comes-back). Wait 1 minute, then retry |
| 502 | `platform.tiktok.api_error` | TikTok returned no usable answer, or accepted a post without returning its publish ID. Check the account in the TikTok app before retrying a post |

A TikTok code that this table does not list returns `502` with the code after `platform.tiktok.`.

### X, LinkedIn, YouTube, and Zalo

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `validation.media_count` | An X post has more media items than X accepts |
| 400 | `validation.media_type` | X does not accept the media type, or a LinkedIn multi-image post includes a video |
| 400 | `validation.media_size` | An X media file is too large to upload in the chunks X allows |
| 402 | `platform.twitter.usage_capped` | Your own X developer app is out of API credits or reached its X usage cap. Add credits or raise the spending limit in the X Developer Console. Waiting does not clear it |
| 422 | `platform.linkedin.processing_failed` | LinkedIn failed to process the media |
| 504 | `platform.linkedin.upload_timeout`, `platform.linkedin.processing_timeout` | A LinkedIn image upload or video processing timed out |
| 403 | `platform.linkedin.access_denied` | LinkedIn denied access |
| 400 | `validation.video_url` | A YouTube video URL is not accessible |
| 400 | `validation.video_size` | The size of the YouTube video file could not be determined |
| 403 | `platform.youtube.insufficientPermissions` | The connection lacks a scope the call needs. Reconnect the account to grant it |
| 403 | `platform.youtube.account_suspended` | YouTube suspended or closed the account or its channel. Reconnecting does not restore access. Resolve the suspension with YouTube. SocialAPI stops background sync for the account |
| 422 | `platform.youtube.processing_failure` | YouTube refused to delete the comment: only comments your channel wrote can be deleted |
| 422 | `platform.youtube.comments_disabled` | The video has comments turned off. Retrying does not help until the channel owner turns them back on, and the error can continue for up to 24 hours after that |

### WhatsApp

WhatsApp has the most codes because of its messaging window, frequency caps, and template states. See [WhatsApp](https://docs.social-api.ai/platforms/whatsapp.md).

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `whatsapp.window_closed` | The 24-hour customer service window is closed. Send an approved template |
| 429 | `whatsapp.frequency_cap` | The recipient reached the daily marketing message limit |
| 429 | `whatsapp.pair_rate_limit` | Too many messages to the same recipient |
| 429 | `whatsapp.throughput_exceeded` | The account's messaging throughput limit is exceeded |
| 403 | `whatsapp.user_opted_out` | The recipient opted out of messages |
| 400 | `whatsapp.undeliverable` | The recipient is not on WhatsApp |
| 404 | `whatsapp.template_not_found` | The template does not exist for this WhatsApp Business account |
| 400 | `whatsapp.template_paused` | The template is paused because of quality issues |
| 400 | `validation.template_invalid` | A template failed validation. `meta.errors` lists the issues |
| 400 | `validation.body_empty` | A template update set no fields |
| 400 | `whatsapp.not_registered` | The number is not registered for the WhatsApp Cloud API. Register it before sending |
| 400 | `whatsapp.pin_mismatch` | The number has a two-step verification PIN SocialAPI does not hold. Turn two-step verification off for the number, then retry |
| 400 | `whatsapp.media_download_failed` | WhatsApp could not download the media from the URL. Check that it is publicly reachable |
| 400 | `whatsapp.media_upload_failed` | WhatsApp rejected the media file |
| 400 | `validation.attachment_id_unsupported` | WhatsApp does not accept `platform_attachment_id`. Send the media as a `url` or `media_id` |
| 400 | `validation.caption_unsupported` | A WhatsApp audio message cannot carry a caption |
| 400 | `validation.image_format_unsupported` | A WhatsApp image is not JPEG or PNG |
| 400 | `validation.voice_unsupported` | `voice` was set on an attachment that is not audio |
| 404 | `whatsapp.no_account` | No WhatsApp account is connected |
| 502 | `whatsapp.unknown` | WhatsApp returned an error SocialAPI does not map. Retry with backoff |

### Telegram

| HTTP | Code | When it occurs |
| - | - | - |
| 403 | `telegram.cannot_message` | The bot cannot message this chat. The person must start the bot first, or the bot was blocked or removed. There is no 24-hour window once the bot is started |
| 400 | `validation.telegram_bad_request` | Telegram rejected the request. Common causes are text over 4,096 characters and an attachment URL Telegram could not fetch. `error.message` carries Telegram's description |

### Zalo

| HTTP | Code | When it occurs |
| - | - | - |
| 422 | `platform.zalo.not_deliverable` | The message is outside the customer service messaging window, or the recipient is not eligible |
| 401 | `platform.zalo.auth` | The access token expired or was revoked, or the Official Account is no longer authorized |
| 202 | `platform.zalo.article_processing` | Zalo is still processing the article when the request stopped waiting. The article can still publish: check before retrying |
| 429 | `platform.zalo.rate_limit` | The per-app or per-Official Account quota is used up |
| 400 | `validation.platform_param` | Zalo rejected a parameter. `error.message` carries Zalo's reason |
| 404 | `resource.not_found` | The Zalo account or object does not exist |
| 502 | `platform.zalo.e<N>` | Zalo returned an error SocialAPI does not map. `<N>` is the absolute value of Zalo's numeric code (Zalo `-105` becomes `platform.zalo.e105`) |
| 502 | `platform.zalo.api_error` | The request failed before Zalo answered with an error |

`platform.zalo.not_deliverable` is the most common: Zalo allows a customer service message within 7 days of the person's last interaction with your Official Account (free within 48 hours), and refuses later sends with `422`.

### Bluesky

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `platform.bluesky.auth_factor_required` | See [Connect an account](#connect-an-account) |
| 403 | `platform.bluesky.dm_scope_missing` | The app password has no direct message access. Create a new app password with the option on, then reconnect. Posts, comments, and mentions keep working |
| 403 | `platform.bluesky.account_takedown` | Bluesky suspended the account. Reconnecting does not restore access |
| 429 | `platform.bluesky.rate_limit` | Bluesky's quota is used up. Sign-in is limited to 30 requests per 5 minutes per account |
| 400 | `validation.field_invalid` | Bluesky could not resolve the handle, or an image is larger than 2 MB. `meta.field` names the field |
| 400 | `validation.platform_param` | Bluesky rejected a parameter. `error.message` carries Bluesky's reason |

`platform.bluesky.dm_scope_missing` affects only the DM endpoints. Treat it as a failure of one capability, not a dead connection.

### Pinterest

| HTTP | Code | When it occurs |
| - | - | - |
| 400 | `board.name_required` | A board has no name |
| 400 | `board.privacy_invalid` | A board's privacy can be changed only to `PUBLIC` or `SECRET` |
| 400 | `board.update_empty` | A board update set no field |
| 403 | `platform.pinterest.403` | Pinterest refused the request for this account or board. `error.message` carries Pinterest's reason. Reconnecting does not clear it |
| 404 | `platform.pinterest.404` | Pinterest reports that the board or Pin does not exist |

## Platform errors

When a platform refuses a call, the code is `platform.<name>.<reason>`, where `<name>` is the platform slug. SocialAPI forwards the platform's HTTP status when it maps one, and returns `502` otherwise.

| HTTP | Code | When it occurs |
| - | - | - |
| 401 | `platform.<name>.auth` | The platform rejected the saved token. Reconnect the account |
| 403 | `platform.twitter.insufficient_scope` | X refused the call because the account's access lacks a scope. `meta.scope` names it for a media upload. The account does not become `token_expired`. Reconnect the account to grant it |
| 401 | `platform.<name>.unauthorized` | The platform answered 401 without saying the token is bad. A healthy account keeps working. Reconnect if it persists |
| 429 | `platform.<name>.rate_limit` | The platform's own limit. See [Rate limits](#rate-limits) |
| 422 | `platform.<name>.publish_failed` | A publish failed with an error SocialAPI does not map. Appears in [Delivery failures](#delivery-failures) |
| 502 | `platform.<name>.api_error` | A failure reaching the platform, or a response with no usable error |
| 502 | `platform.<name>.<code>` | The platform returned its own error code. The status is `502` unless the code has its own row above, such as `404 platform.facebook.100` |

Branch on the prefix and suffix: `platform.` with `.auth` means reconnect, with `.rate_limit` means back off.

## System errors

| HTTP | Code | When it occurs |
| - | - | - |
| 500 | `system.internal` | The request failed on SocialAPI's side. Retry idempotent requests. If it persists, send the `request_id` to support |
| 503 | `system.internal` | The host of a media URL in the request could not be looked up at that moment. `meta.field` is `media`. Nothing was created or changed. Retry the request |

## Handling errors

Read `error.code` from the response, then branch. Reconnect on a dead token, back off on a throttle, show the field on a validation failure, and log `request_id` for anything else.

```typescript theme={null}
const resp = await fetch("https://api.social-api.ai/v1/accounts", {
  headers: { Authorization: `Bearer ${process.env.SOCAPI_KEY}` },
});

if (!resp.ok) {
  const { error, request_id } = await resp.json();
  const { code, message, meta } = error;

  if (code === "account.reconnection_required" ||
      (code.startsWith("platform.") && code.endsWith(".auth"))) {
    return reconnect(); // POST /v1/accounts/connect
  }
  if (resp.status === 429 && meta?.retry_after_seconds) {
    await sleep(meta.retry_after_seconds * 1000);
    return retry();
  }
  if (code.startsWith("validation.")) {
    return showFieldError(meta?.field, message);
  }
  throw new Error(`${code}: ${message} (request_id ${request_id})`);
}
```

For a `429` without `meta.retry_after_seconds` (a platform limit), wait and retry with backoff. A plan allowance such as `validation.post_limit_exhausted` does not clear until the billing period ends: check `GET /v1/usage`.

## Related

* [Debugging](https://docs.social-api.ai/guides/debugging.md) for `X-Request-ID` and what to send to support
* [Rate limits](https://docs.social-api.ai/guides/rate-limits.md) for every 429 code, its window, and how to retry
* [Plans and usage](https://docs.social-api.ai/guides/plans.md) for allowances and past-due accounts
* [Track post delivery](https://docs.social-api.ai/guides/delivery.md) for target errors on a post
* [Scoped API keys](https://docs.social-api.ai/guides/scoped-keys.md) for `auth.insufficient_scope`
* [Authentication](https://docs.social-api.ai/guides/authentication.md) for API keys and OAuth 2.1 access tokens


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