# Errors (/docs/api/errors)



Error response format [#error-response-format]

Every error has the same shape:

```json
{
    "error": {
        "type": "invalid_request_error",
        "code": "validation_failed",
        "message": "content: Too big: expected string to have <=63206 characters",
        "param": "content",
        "details": [
            { "param": "content", "message": "content: Too big: expected string to have <=63206 characters" }
        ],
        "docUrl": "https://feedhorizon.com/docs/api/errors#invalid_request_error",
        "requestId": "req_m2x81a0kfd3h9s"
    }
}
```

| Field       | Stable | Description                                                                                                                         |
| ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `type`      | Yes    | Error class, one of the headings below.                                                                                             |
| `code`      | Yes    | Machine-readable code in `snake_case`. Branch on this.                                                                              |
| `message`   | No     | For humans. May be reworded between releases, never branch on it.                                                                   |
| `param`     | Yes    | The field or parameter at fault, dotted when nested (`targets.0.accountId`).                                                        |
| `details`   | Yes    | One entry per problem, on `validation_failed` and on post checks. Each has `param`, `message` and, for post checks, its own `code`. |
| `docUrl`    | Yes    | Link to the section of this page for the error type.                                                                                |
| `requestId` | Yes    | The same value as the `x-request-id` header. Quote it when you contact support.                                                     |

Error types [#error-types]

invalid_request_error [#invalid_request_error]

The request is malformed or breaks a rule. Fix it before retrying.

**Request format**

Status `400`.

| Code                | Meaning                                                                              |
| ------------------- | ------------------------------------------------------------------------------------ |
| `invalid_json`      | The body is not valid JSON                                                           |
| `validation_failed` | One or more fields are invalid, see `details`                                        |
| `invalid_request`   | The request breaks a rule not covered by a more specific code; `message` says which  |
| `invalid_cursor`    | `cursor` is not a value the API returned as `nextCursor`                             |
| `profile_not_found` | A `profileId` in the body does not exist                                             |
| `platform_mismatch` | The endpoint does not fit this account's platform, e.g. boards of a LinkedIn account |
| `invalid_slot`      | A queue slot has an invalid `dayOfWeek` or `time`                                    |

**Post checks.** A post is checked against every platform before it is stored. With one problem, its code is `error.code`. With several, `error.code` is `post_invalid` and each entry in `details` carries its own `code`. [`POST /posts/validate`](/docs/guides/scheduling#check-before-you-create) returns the same codes without creating anything.

Status `400`.

| Code                             | Meaning                                                                                                                        |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `post_invalid`                   | Several problems at once, see `details[].code`                                                                                 |
| `content_required`               | A post needs text or media                                                                                                     |
| `content_too_long`               | The text is longer than a platform allows (`param` names which text)                                                           |
| `no_targets`                     | A post needs `platforms`, `accountIds` or `targets`                                                                            |
| `no_connected_account`           | The profile has no active account for a requested platform                                                                     |
| `account_not_in_profile`         | An account belongs to another profile                                                                                          |
| `duplicate_account`              | The same account appears twice in `targets`                                                                                    |
| `platform_unavailable`           | The platform is planned but not live yet                                                                                       |
| `schedule_in_past`               | `scheduledFor` lies in the past                                                                                                |
| `invalid_schedule`               | `scheduledFor` is not a valid date                                                                                             |
| `queue_not_configured`           | `addToQueue` was sent but the profile has no queue slots                                                                       |
| `media_required`                 | The platform needs media (Instagram and Pinterest any, TikTok a video)                                                         |
| `too_many_media`                 | More files than the platform takes                                                                                             |
| `media_type_unsupported`         | A file type the platform does not take. Also answered by `POST /media` for a type FeedHorizon does not store                   |
| `media_too_large`                | A file is bigger than the platform allows. Also answered by `POST /media` for an import over 100 MB or an upload over 5 GB     |
| `media_not_found`                | A `mediaIds` entry does not exist in your organization                                                                         |
| `duplicate_media`                | The same file appears twice in `mediaIds`                                                                                      |
| `media_not_uploaded`             | The file was announced but never uploaded                                                                                      |
| `tiktok_privacy_required`        | `options.tiktok.privacyLevel` is missing                                                                                       |
| `tiktok_branded_content_private` | `brandedContent` cannot be combined with `SELF_ONLY`                                                                           |
| `account_not_active`             | A **warning**, not an error: the account needs a reconnect. The post is stored, but that account fails until it is reconnected |

**Media** (`POST /media`)

Status `400`.

| Code                 | Meaning                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `invalid_url`        | Not a valid `https` URL, a URL with credentials, or one that points to a private network |
| `media_fetch_failed` | The URL could not be fetched or answered with an error                                   |

authentication_error [#authentication_error]

Status `401`.

| Code              | Meaning                           |
| ----------------- | --------------------------------- |
| `missing_api_key` | No `Authorization: Bearer` header |
| `invalid_api_key` | Unknown, revoked or expired key   |

permission_error [#permission_error]

Status `403`.

| Code                    | Meaning                                                                                                                    |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `insufficient_scope`    | The key lacks the scope for this endpoint                                                                                  |
| `profile_access_denied` | The key is limited to other profiles                                                                                       |
| `all_profiles_required` | The endpoint manages the whole organization (API keys, webhooks, new profiles) and needs a key with access to all profiles |

not_found_error [#not_found_error]

Status `404`.

| Code                 | Meaning                                                                                                                       |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `resource_not_found` | The resource does not exist, belongs to another organization, or the key cannot see its profile. A malformed id is also a 404 |

conflict_error [#conflict_error]

The request is valid but the current state does not allow it.

Status `409`.

| Code                    | Meaning                                                     |
| ----------------------- | ----------------------------------------------------------- |
| `post_not_editable`     | The post is publishing or already published                 |
| `post_publishing`       | The post is being published right now and cannot be deleted |
| `post_not_failed`       | Only failed posts can be retried                            |
| `post_not_scheduled`    | Only scheduled posts can be cancelled                       |
| `media_in_use`          | The file is still used by a post                            |
| `external_id_taken`     | Another profile already uses this `externalId`              |
| `already_exists`        | A resource with these values already exists                 |
| `queue_full`            | No free queue slot in the next 8 weeks                      |
| `default_profile`       | The default profile cannot be deleted                       |
| `webhook_limit_reached` | The organization already has 10 webhooks                    |
| `key_in_use`            | A key cannot revoke itself                                  |
| `invalid_state`         | The current state does not allow this; `message` says why   |

idempotency_error [#idempotency_error]

See [Idempotency](/docs/api/idempotency).

| Status | Code                              | Meaning                                                                  |
| ------ | --------------------------------- | ------------------------------------------------------------------------ |
| `400`  | `invalid_idempotency_key`         | The key is empty or longer than 255 characters                           |
| `409`  | `idempotency_request_in_progress` | The first request with this key is still running. Retry in a few seconds |
| `422`  | `idempotency_key_reused`          | The key was used for a different request. Use a new key                  |

rate_limit_error [#rate_limit_error]

Status `429`.

| Code           | Meaning                                              |
| -------------- | ---------------------------------------------------- |
| `rate_limited` | Too many requests. Wait the seconds in `Retry-After` |

See [Rate Limits](/docs/api/rate-limits).

Publishing errors [#publishing-errors]

Errors that happen after a post was accepted, while it goes out, do not come back as an HTTP error. They land on the account's target in the post, `targets[].error`, and in the `post.target.failed` webhook:

```json
{
  "code": "platform_rejected",
  "category": "rejected",
  "source": "user",
  "message": "LinkedIn publish failed (422): Content is a duplicate of urn:li:share:7654…"
}
```

Branch on `code`, or on `category` and `source` for a coarser decision. `source` says who can fix it: `user` (you or your customer), `platform` (wait, FeedHorizon retries) or `system` (FeedHorizon). `message` carries the platform's own reason and may change.

| Code                    | Category         | Source     | Retried | What to do                                                                                                           |
| ----------------------- | ---------------- | ---------- | ------- | -------------------------------------------------------------------------------------------------------------------- |
| `account_needs_reauth`  | `auth_expired`   | `user`     | No      | Reconnect the account, then `POST /posts/{id}/retry`                                                                 |
| `platform_auth_failed`  | `auth_expired`   | `user`     | No      | The platform refused the token or a permission is missing. Reconnect, then retry                                     |
| `platform_rate_limited` | `rate_limited`   | `platform` | Yes     | Nothing; FeedHorizon tries again. Space out posts to this account                                                    |
| `platform_rejected`     | `rejected`       | `user`     | No      | The platform refused this content (duplicate, format, policy). Change it, then retry                                 |
| `platform_unavailable`  | `platform_error` | `platform` | Yes     | Nothing; FeedHorizon tries again                                                                                     |
| `timeout`               | `timeout`        | `system`   | No      | Publishing did not finish within 10 minutes. Check the platform first: the post may be live. Retry only if it is not |
| `publish_failed`        | `unknown`        | `system`   | Yes     | Read `message`; contact support with the post id if it persists                                                      |

Retried errors get 3 attempts in total, after 2 and 10 minutes. Only the final outcome sends `post.target.failed`.

api_error [#api_error]

Status `500`.

| Code             | Meaning                                                                                       |
| ---------------- | --------------------------------------------------------------------------------------------- |
| `internal_error` | Something failed on our side. Retry with backoff, using the same `Idempotency-Key` for writes |

Handling errors [#handling-errors]

```javascript title="Node.js"
const res = await fetch('https://feedhorizon.com/api/v1/posts', {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer fh_YOUR_API_KEY',
        'Content-Type': 'application/json',
        'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify({ profileId: 'YOUR_PROFILE_ID', platforms: ['linkedin'], content: 'Hello!' }),
});

if (!res.ok) {
    const { error } = await res.json();
    if (error.code === 'validation_failed') {
        for (const d of error.details) console.error(`${d.param}: ${d.message}`);
    } else {
        console.error(`${error.type}/${error.code}: ${error.message} (${error.requestId})`);
    }
}
```

When to retry [#when-to-retry]

| Status                            | Retry?    | How                                                                       |
| --------------------------------- | --------- | ------------------------------------------------------------------------- |
| `400`, `401`, `403`, `404`, `422` | No        | Fix the request, key or id                                                |
| `409`                             | Sometimes | Only `idempotency_request_in_progress`, after a few seconds               |
| `429`                             | Yes       | After `Retry-After` seconds                                               |
| `500`                             | Yes       | Exponential backoff (1 s, 2 s, 4 s, at most 30 s), same `Idempotency-Key` |
