Errors
One error format for every endpoint, stable codes, and when to retry.
Error response format
Every error has the same shape:
{
"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
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 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
Status 401.
| Code | Meaning |
|---|---|
missing_api_key | No Authorization: Bearer header |
invalid_api_key | Unknown, revoked or expired key |
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
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
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
See 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
Status 429.
| Code | Meaning |
|---|---|
rate_limited | Too many requests. Wait the seconds in Retry-After |
See Rate Limits.
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:
{
"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
Status 500.
| Code | Meaning |
|---|---|
internal_error | Something failed on our side. Retry with backoff, using the same Idempotency-Key for writes |
Handling errors
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
| 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 |