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:
{
"error": {
"code": "content_too_long",
"message": "Text exceeds the 280-character limit for X/Twitter.",
"status": 400
}
}
| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code (see table below) |
error.message | string | Human-readable explanation of what went wrong |
error.status | number | HTTP status code |
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. |
content_too_long | Post text exceeds the platform's character limit. |
too_many_accounts | The accountIds 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 scheduledAt value is not a valid ISO 8601 datetime string. |
past_schedule_time | The scheduledAt 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. |
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
| Status | Retry? | Reason |
|---|---|---|
| 400 | No | Fix the request. The same payload will always fail. |
| 401 | No | Fix your API key or generate a new one. |
| 402 | No | Upgrade your plan or wait for credits to reset. |
| 403 | No | Check your API key scopes or modify the request. |
| 404 | No | The resource does not exist. |
| 429 | Yes | Wait for the X-RateLimit-Reset timestamp, then retry. |
| 500 | Yes | 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
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 30 seconds.",
"status": 429
}
}
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