Errors
One error format for every endpoint, stable codes, and when to retry.
Diese Seite gibt es noch nicht auf Deutsch, deshalb siehst du sie auf Englisch.
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 |
account_limit_reached | The free plan's 2 social accounts are connected; start the paid plan to connect more |
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 |