Skip to main content

Error Handling

The API uses conventional HTTP status codes and returns a consistent JSON error envelope so you can handle failures programmatically.

Error Response Format

Every error response follows the same structure:

{
"data": null,
"error": {
"message": "Free X accounts are limited to 280 characters per post (currently 282).",
"code": "post_creation_failed",
"retryable": false,
"details": {
"validation_errors": [...]
}
},
"meta": {
"request_id": "req_01HXYZ...",
"timestamp": "2026-04-12T14:30:00Z"
}
}
FieldTypeDescription
datanullAlways null on error responses.
error.messagestringHuman-readable explanation of what went wrong.
error.codestringMachine-readable error code (see table below).
error.retryablebooleanWhether retrying this exact request might succeed. true for transient failures (5xx, 429); false for permanent failures (most 4xx). Use this instead of inferring from the status code.
error.detailsobjectOptional. Additional context about the error (e.g. failed validation fields, circuit breaker info).
meta.request_idstringUnique ID for this request. Include when contacting support.
meta.timestampstringISO 8601 timestamp of when the response was generated.

The HTTP status code is returned in the response status line — there is no error.status field in the body (removed 2026-04-12).

Always check error.retryable

The simplest robust retry logic: retry if error.retryable === true, abort otherwise. This is more reliable than inferring from the status code and protects you from edge cases like our circuit breaker (see 422 below).

Error Codes by HTTP Status

400 Bad Request

Validation or input errors. Do not retry these -- fix the request first.

CodeDescription
validation_errorRequest body failed schema validation. Check required fields and types.
invalid_jsonThe request body is not valid JSON. Common cause: unescaped newlines/tabs inside a string value — use \n / \t, not raw control characters.
content_too_longPost text exceeds the platform's character limit.
too_many_accountsThe account_ids array exceeds the maximum number of accounts per request.
invalid_timezoneThe timezone value is not a valid IANA timezone string.
invalid_accountsOne or more account IDs do not exist or do not belong to your organization.
invalid_datetimeThe scheduled_for value is not a valid ISO 8601 datetime string.
past_schedule_timeThe scheduled_for time is in the past. Schedule at least 5 minutes ahead.
invalid_post_statusThe post is in a status that does not allow the requested action.
no_failed_destinationsRetry was called on a post with no failed platform destinations.
unsupported_media_typeThe uploaded file MIME type is not supported. See Media Requirements.
file_too_largeThe uploaded file exceeds the size limit (20 MB images, 500 MB videos).
upload_failedThe file upload could not be processed. Try uploading again.
file_not_uploadedThe media ID references a file that has not finished uploading or does not exist.
file_type_mismatchThe uploaded file's actual type does not match the declared MIME type.

401 Unauthorized

Authentication failures. Check your API key.

CodeDescription
invalid_api_keyThe API key in the Authorization header is not valid.
api_key_revokedThe API key has been revoked. Generate a new key in Settings > Developers.
api_key_expiredThe API key has expired. Generate a new key in Settings > Developers.

402 Payment Required

Billing or entitlement issues.

CodeDescription
insufficient_creditsYour plan's AI credits have been exhausted for this billing period.
upgrade_requiredYour current plan does not include this feature. Upgrade your plan.

403 Forbidden

Permission or scope errors.

CodeDescription
insufficient_scopeYour API key does not have the required scope for this endpoint. See Scopes.
prompt_rejectedThe AI content generation request was rejected by the safety filter.

404 Not Found

CodeDescription
not_foundThe requested resource does not exist or does not belong to your organization.

422 Unprocessable Entity

CodeDescription
permanent_failure_circuit_breakerThis exact request body has failed 5+ times in the last 6 hours and the circuit breaker is now open for it. Modify any field of the request body (the content_hash in details will change) — or wait 6 hours — to retry. Triggered when an integration ignores 4xx responses and retries the same payload in a loop.

When the circuit breaker is open, the response includes:

{
"data": null,
"error": {
"message": "This exact request has failed 5 times in the last 6 hours...",
"code": "permanent_failure_circuit_breaker",
"retryable": false,
"details": {
"circuit_breaker_open": true,
"content_hash": "a1b2c3d4e5f6g7h8",
"fail_count": 5,
"threshold": 5,
"ttl_seconds": 21600,
"hint": "The previous failures should have included `validation_errors` explaining what to fix..."
}
},
"meta": { "request_id": "req_01HXYZ...", "timestamp": "..." }
}

To clear the breaker before its 6-hour TTL, change any field of the request body — even adding an idempotency_key you weren't using before will produce a new content hash and reset the counter to 0. The breaker exists specifically to protect badly-written retry loops from burning rate-limit budget on requests that will never succeed. If you're hitting it, inspect the validation_errors from the previous 4xx responses — those tell you exactly what to fix.

429 Too Many Requests

CodeDescription
rate_limit_exceededYou have exceeded the per-minute or per-hour rate limit. See Rate Limits.

500 Internal Server Error

CodeDescription
generation_failedAn AI content generation request failed internally. Safe to retry.
service_unavailableA downstream service (platform API, media processing) is temporarily unavailable. Safe to retry.
internal_errorAn unexpected server error occurred. Safe to retry with backoff.

Which Errors to Retry

Recommended: check error.retryable (boolean) on the response — it tells you directly whether retrying makes sense, without needing to map status codes yourself.

StatusretryableReason
400falseFix the request. The same payload will always fail.
401falseFix your API key or generate a new one.
402falseUpgrade your plan or wait for credits to reset.
403falseCheck your API key scopes or modify the request.
404falseThe resource does not exist.
422falseCircuit breaker open — modify the request body before retrying (see above). Retrying the same payload will fail until the 6-hour TTL expires.
429trueWait for the Retry-After header value, then retry.
500trueRetry with exponential backoff (see below).

Retry Strategy

For retryable errors (429 and 5xx), use exponential backoff with jitter:

async function callWithRetry(fn, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fn();

if (response.ok) {
return response.json();
}

const status = response.status;

// Don't retry client errors (except 429)
if (status >= 400 && status < 500 && status !== 429) {
const body = await response.json();
throw new Error(`${body.error.code}: ${body.error.message}`);
}

if (attempt === maxRetries) {
throw new Error(`Request failed after ${maxRetries + 1} attempts`);
}

// For 429, respect the Retry-After header if present
const retryAfter = response.headers.get("Retry-After");
if (retryAfter) {
await sleep(parseInt(retryAfter, 10) * 1000);
continue;
}

// Exponential backoff: 1s, 2s, 4s, 8s, 16s + random jitter
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
await sleep(baseDelay + jitter);
}
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
# Example: a 429 response with rate-limit headers
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711929660
Retry-After: 30

{
"data": null,
"error": {
"message": "Rate limit exceeded. Try again in 30 seconds.",
"code": "rate_limit_exceeded"
},
"meta": {
"request_id": "req_01HXYZ...",
"timestamp": "2026-04-12T14:30:00Z"
}
}

Best Practices

  1. Always check the error.code field -- not just the HTTP status. Multiple error codes can share the same status.
  2. Log the full error response for debugging. The message field provides useful context.
  3. Set a retry budget. Cap retries at 5 attempts to avoid infinite loops.
  4. Handle 429 specifically. Respect the Retry-After header when present instead of using your own backoff timer.
  5. Distinguish transient from permanent errors. Only retry 429 and 5xx codes.