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"
}
}
| Field | Type | Description |
|---|---|---|
data | null | Always null on error responses. |
error.message | string | Human-readable explanation of what went wrong. |
error.code | string | Machine-readable error code (see table below). |
error.retryable | boolean | Whether 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.details | object | Optional. Additional context about the error (e.g. failed validation fields, circuit breaker info). |
meta.request_id | string | Unique ID for this request. Include when contacting support. |
meta.timestamp | string | ISO 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).
error.retryableThe 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.
| Code | Description |
|---|---|
validation_error | Request body failed schema validation. Check required fields and types. |
invalid_json | The request body is not valid JSON. Common cause: unescaped newlines/tabs inside a string value — use \n / \t, not raw control characters. |
content_too_long | Post text exceeds the platform's character limit. |
too_many_accounts | The account_ids array exceeds the maximum number of accounts per request. |
invalid_timezone | The timezone value is not a valid IANA timezone string. |
invalid_accounts | One or more account IDs do not exist or do not belong to your organization. |
invalid_datetime | The scheduled_for value is not a valid ISO 8601 datetime string. |
past_schedule_time | The scheduled_for time is in the past. Schedule at least 5 minutes ahead. |
invalid_post_status | The post is in a status that does not allow the requested action. |
no_failed_destinations | Retry was called on a post with no failed platform destinations. |
unsupported_media_type | The uploaded file MIME type is not supported. See Media Requirements. |
file_too_large | The uploaded file exceeds the size limit (20 MB images, 500 MB videos). |
upload_failed | The file upload could not be processed. Try uploading again. |
file_not_uploaded | The media ID references a file that has not finished uploading or does not exist. |
file_type_mismatch | The uploaded file's actual type does not match the declared MIME type. |
401 Unauthorized
Authentication failures. Check your API key.
| Code | Description |
|---|---|
invalid_api_key | The API key in the Authorization header is not valid. |
api_key_revoked | The API key has been revoked. Generate a new key in Settings > Developers. |
api_key_expired | The API key has expired. Generate a new key in Settings > Developers. |
402 Payment Required
Billing or entitlement issues.
| Code | Description |
|---|---|
insufficient_credits | Your plan's AI credits have been exhausted for this billing period. |
upgrade_required | Your current plan does not include this feature. Upgrade your plan. |
403 Forbidden
Permission or scope errors.
| Code | Description |
|---|---|
insufficient_scope | Your API key does not have the required scope for this endpoint. See Scopes. |
prompt_rejected | The AI content generation request was rejected by the safety filter. |
404 Not Found
| Code | Description |
|---|---|
not_found | The requested resource does not exist or does not belong to your organization. |
422 Unprocessable Entity
| Code | Description |
|---|---|
permanent_failure_circuit_breaker | This 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
| Code | Description |
|---|---|
rate_limit_exceeded | You have exceeded the per-minute or per-hour rate limit. See Rate Limits. |
500 Internal Server Error
| Code | Description |
|---|---|
generation_failed | An AI content generation request failed internally. Safe to retry. |
service_unavailable | A downstream service (platform API, media processing) is temporarily unavailable. Safe to retry. |
internal_error | An 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.
| Status | retryable | Reason |
|---|---|---|
| 400 | false | Fix the request. The same payload will always fail. |
| 401 | false | Fix your API key or generate a new one. |
| 402 | false | Upgrade your plan or wait for credits to reset. |
| 403 | false | Check your API key scopes or modify the request. |
| 404 | false | The resource does not exist. |
| 422 | false | Circuit breaker open — modify the request body before retrying (see above). Retrying the same payload will fail until the 6-hour TTL expires. |
| 429 | true | Wait for the Retry-After header value, then retry. |
| 500 | true | Retry 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
- Always check the
error.codefield -- not just the HTTP status. Multiple error codes can share the same status. - Log the full error response for debugging. The
messagefield provides useful context. - Set a retry budget. Cap retries at 5 attempts to avoid infinite loops.
- Handle 429 specifically. Respect the
Retry-Afterheader when present instead of using your own backoff timer. - Distinguish transient from permanent errors. Only retry 429 and 5xx codes.
Related
- Authentication -- API key setup and usage
- Rate Limits -- per-minute and per-hour limits, response headers
- Scopes -- API key permission scopes