# PostEverywhere Error Handling Reference > Complete reference of PostEverywhere API error codes, HTTP status codes, and retry strategies. Handle errors gracefully in your integration. **Source:** https://posteverywhere.ai/docs/errors **Section:** Reference **API reference:** https://posteverywhere.ai/docs/api/reference --- The API uses conventional HTTP status codes and returns a consistent JSON error envelope so you can handle failures programmatically. ## The shape of every error response Every endpoint error follows the same structure. The one exception is the authentication layer: `401` codes (`invalid_api_key`, `api_key_revoked`, `api_key_expired`) and `402 subscription_required` omit `retryable` and `meta.request_id`, and a request with no `Authorization` header at all returns a plain-text `401 Unauthorized`. ```json { "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": "a1b2c3d4", "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). > **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](#422-unprocessable-entity)). ## Every error code, grouped 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. Any future instant is accepted (`POST /v1/posts/{id}/schedule` requires at least 1 minute 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](/docs/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. | | `bulk_limit_exceeded` | `POST /v1/posts/bulk` received more than 50 posts in one call. Split into multiple calls. Also fires for `posts/retry-failed` with `post_ids > 200`. | | `no_filter` | `POST /v1/posts/retry-failed` was called with no filter. At least one of `post_ids`, `account_id`, `platform`, `failed_after`, `failed_before` is required. | | `invalid_period` | `GET /v1/analytics/summary` received an invalid `period` value. Must be `today`, `week`, `month`, `all`, or `custom`. | | `invalid_range` | `GET /v1/analytics/summary?period=custom` is missing `from`/`to` or they're not valid ISO timestamps. | | `invalid_event_type` | `POST /v1/webhooks` received an unknown event name. The response `details.valid_events` lists every accepted event. | | `limit_reached` | The org has reached the per-org cap (e.g. 25 webhooks). Contact support to raise. | | `prompt_rejected` | The AI content generation request was rejected by the safety filter. | ### 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. Create a new key on the **Developers** page. | | `api_key_expired` | The API key has expired. Create a new key on the **Developers** page. | | `api_key_required` | No API key was supplied on an endpoint that requires one (social-connect flows). | | `auth_failed` | The platform rejected the supplied credentials during a social-connect flow. | ### 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](https://app.posteverywhere.ai/choose-plan). | | `subscription_required` | The organization has no active subscription or trial; the body includes `subscription_status`. | ### 403 Forbidden Permission or scope errors. | Code | Description | |------|-------------| | `insufficient_scope` | Your API key does not have the required scope for this endpoint. See [Scopes](/docs/scopes). | ### 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: ```json { "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": "a1b2c3d4", "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](/docs/rate-limits). | | `post_limit_reached` | The organization's post allowance is exhausted (`POST /v1/posts/{id}/schedule`). | | `daily_limit_reached` | The daily posting cap is exhausted (`POST /v1/posts/{id}/schedule`). | ### 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 are worth retrying **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). | ## How to back off and retry safely For retryable errors (429 and 5xx), use exponential backoff with jitter: ```javascript 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)); } ``` ```bash # 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: 120 { "data": null, "error": { "message": "API rate limit exceeded (60 requests/min). Please slow down.", "code": "rate_limit_exceeded", "retryable": true, "details": { "limit": 60, "remaining": 0, "reset_at": "2026-04-12T14:31:00.000Z" } }, "meta": { "request_id": "a1b2c3d4", "timestamp": "2026-04-12T14:30:00Z" } } ``` ## Handling errors without losing posts 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. ## Related error and reliability pages - [Authentication](/docs/authentication) -- API key setup and usage - [Rate Limits](/docs/rate-limits) -- per-minute and per-hour limits, response headers - [Scopes](/docs/scopes) -- API key permission scopes **Related:** [reproducing errors safely](/docs/testing) · [list endpoint parameters](/docs/pagination) · [failure events by webhook](/docs/webhooks) · [newly added error codes](/docs/changelog)