# Errors

Errors come back as JSON with a stable `code`, a sentence you can show to a person, and sometimes `details`:

```json
{
  "error": {
    "code": "VALIDATION",
    "message": "Some posts need changes before they can be scheduled",
    "details": {
      "preflight": [
        {
          "socialAccountId": "acc_th",
          "issues": [{ "severity": "error", "code": "caption_length", "message": "Caption is 612 characters; the limit is 500." }]
        }
      ]
    }
  }
}
```

## Codes

| HTTP | `code` | Meaning and what to do |
|---|---|---|
| 400 | `VALIDATION` | The request needs fixing. `details` is either field errors (`{ "scheduledAt": ["Required"] }`) or `preflight` issues per account. |
| 401 | `UNAUTHENTICATED` | Missing, malformed, expired or deleted key or token. OAuth clients refresh the token. |
| 402 | `SUBSCRIPTION_EXPIRED`, `SUBSCRIPTION_GRACE` | The subscription isn't active. Reading still works; renew in the app to write. |
| 402 | `PLAN_LIMIT` | A plan quota is used up: posts a month, accounts or storage. The message says which. |
| 403 | `FORBIDDEN` | A read-only credential, an owner-only action, or a workspace that needs the owner's approval. |
| 404 | `NOT_FOUND` | Not in this workspace (or deleted). |
| 409 | `CONFLICT` | The post is publishing right now, it changed since you read it (`expectedVersion`), nothing to retry, or the platform refused an action (the message explains). |
| 429 | `RATE_LIMITED` | Too many requests. Wait `Retry-After` seconds. |
| 500 | `INTERNAL` | Something broke on our side. Retry later; tell us if it keeps happening. |

Retry `429`, `500` and network errors with backoff. Don't retry `400`, `401`, `402`, `403` or `404` unchanged.

## Pre-flight issues

When a post is scheduled, published or [validated](/api/operations/validatepost/), each target is checked against its
[content type's rules](/platforms/). Errors block scheduling; warnings don't. Drafts aren't checked.

| Code | Severity | Means |
|---|---|---|
| `empty_post` | error | No caption and no media. |
| `caption_length` | error | Caption over the platform's limit. |
| `caption_hashtags`, `caption_mentions` | error | Too many hashtags or @mentions (Instagram: 30 and 20). |
| `media_count` | error | Too few or too many files for the format. |
| `media_kind` | error | Photos where a video is needed, or the reverse. |
| `media_format` | error | File type not accepted (e.g. PNG for an Instagram photo, which must be JPEG). |
| `media_size` | error | File too large. |
| `media_aspect` | error | Width ÷ height outside the allowed range. |
| `media_duration` | error | Video too short or too long. |
| `follow_up_length` | error | A follow-up comment is too long. |
| `youtube_title`, `youtube_tags`, `youtube_thumbnail`, `youtube_angle_brackets` | error | YouTube metadata limits. |
| `media_aspect_recommended` | warning | Allowed, but not the shape the platform recommends. |
| `media_dimensions_unknown`, `media_duration_unknown` | warning | PostFlow couldn't measure a video; send `width`, `height` and `durationMs` when importing. |
| `caption_unsupported`, `first_comment_unsupported`, `follow_up_unsupported` | warning | The format ignores it (e.g. captions on Stories). |
| `youtube_long` | warning | Long videos need a phone-verified YouTube channel. |

## After scheduling

Errors from the platform at publishing time don't fail your request; they show up on the post's target with `status: "failed"`,
`errorCode` and a plain `errorMessage`, and as a [`post.failed` webhook](/guides/webhooks/). See [Post statuses](/platforms/statuses/).