PostEverywhere Error Handling Reference
Complete reference of PostEverywhere API error codes, HTTP status codes, and retry strategies. Handle errors gracefully in your integration.
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.
{
"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, also sent as the X-Request-Id header. Search for it in Logs, and include it 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.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).
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 (or a title or description field) is over a platform's real limit, so the post is refused on create or edit instead of failing when it publishes. details.platform_errors[] gives each platform with its current_length and max_length. See the limits below. |
x_poll_invalid |
An X poll breaks X's rules: it needs 2 to 4 options of up to 25 characters and a duration from 5 minutes to 7 days, with no images, video or quoted post. Refused when the post is created or edited. |
unsupported_schedule_at |
schedule_at was sent to the REST API. It is an MCP option. Read GET /v1/best-times and send the time as scheduled_for. |
request_approval_needs_draft |
request_approval: true was sent without draft: true. Nothing was created. |
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. |
file_too_large |
The uploaded file exceeds the size limit (20 MB images, 4 GB 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. |
wordpress_post_invalid |
The WordPress settings in platform_content.wordpress failed validation; details.errors[] lists every problem. See the WordPress guide. |
wordpress_title_required |
A WordPress post has no settings.title and an empty body, so no title can be derived. Any body text avoids it, because the first line becomes the title. |
x_article_invalid |
An X Article request failed validation. details.errors[] lists every problem: non-X accounts in account_ids (articles publish to X only), a missing title or a title over 100 characters, an empty body, or a body over 25,000 characters in platform_content.x.content. See Publishing an X Article. |
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. |
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. |
404 Not Found
| Code | Description |
|---|---|
not_found |
The requested resource does not exist or does not belong to your organization. Also returned, as JSON, for a path that isn't an API endpoint (for example a typo in the URL). |
422 Unprocessable Entity
| Code | Description |
|---|---|
idempotency_key_reused |
The Idempotency-Key was already used with a different request body. Use a new key for a new request. See Make retries safe. |
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": "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.
409 Conflict
| Code | Description |
|---|---|
idempotency_request_in_progress |
A request with the same Idempotency-Key is still running. Wait for the Retry-After header (2 seconds) and retry with the same key. |
media_in_use |
DELETE /v1/media/{id} on a file that a post not yet published still uses. Nothing is deleted. Retry with ?force=true only if those posts should be cancelled: they are marked failed and never publish. |
429 Too Many Requests
| Code | Description |
|---|---|
rate_limit_exceeded |
You have exceeded the per-minute or per-hour rate limit. See 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. |
Failures reported on the post, not the request
Some problems can only be known when a post publishes, so they appear on the failed destination (and in post.failed webhooks) rather than as an HTTP error on create. For X Articles:
| Failure | What it means | What to do |
|---|---|---|
| X Premium required | The X account has no X Premium subscription, which X requires for articles. | Subscribe the account to X Premium, or send the content as a regular post or thread. |
| PostEverywhere article limit | Each X account can publish 2 X Articles in any rolling 24 hours. Only successful publishes count. The post fails with X_ARTICLE_DAILY_LIMIT. |
Reschedule the article for the next day. |
| X's own article limit | X caps article actions at 10 per X account per 24 hours. The post is not failed: it is held and publishes automatically 5 minutes after X's reset, and the message states that time (UTC). It fails with X_ARTICLE_X_DAILY_LIMIT only if X is still refusing after three holds. |
Nothing, unless it fails; then try again the next day. |
Caption character limits behind content_too_long
| Platform | Limit |
|---|---|
| 2,200 | |
| 3,000 | |
| 63,206 | |
| description 500, title 100 | |
| Discord | 2,000 |
| YouTube | title 100, description 5,000 (bytes) |
| TikTok | 2,200 for a video caption (photo posts: description 4,000, title 90) |
| X | 280 without X Premium, 25,000 with it (X's own count: a link counts 23, most emoji 2) |
| Threads | 500 |
| Bluesky | 300 |
| Google Business Profile | 1,500 |
X Articles and WordPress posts have their own limits (see the platform guides). Telegram splits long text into several messages, so it has no limit here.
Published, but with a warning
A post that goes out with a problem, such as text cut to fit or a setting the platform did not apply, is not a failure. Each platform's entry in GET /v1/posts/{id} and GET /v1/posts/{id}/results has a warnings array (empty when there is nothing to report), each with a code and a message. Show these to the user. Each warning also fires the post.warning webhook.
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:
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: 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
- 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 error and reliability pages
- Authentication -- API key setup and usage
- Rate Limits -- per-minute and per-hour limits, response headers
- Scopes -- API key permission scopes
Related: reproducing errors safely · list endpoint parameters · failure events by webhook · newly added error codes
Make retries safe with an Idempotency-Key
Pass an Idempotency-Key header on POST /v1/posts and POST /v1/posts/bulk to make retries safe. If a network error or timeout leaves you unsure whether a post was created, send the same request again with the same key:
- Same key, same body: you get the original response back, with the header
Idempotent-Replayed: true. Nothing new is created and nothing publishes twice. - Same key, different body:
422 idempotency_key_reused. - First request still running:
409 idempotency_request_in_progresswithRetry-After: 2. - First request failed with a 5xx: the key is released, so your retry runs normally.
Keys are any unique string up to 255 characters (a UUID works well), last 24 hours, and are scoped to your API key.
curl -X POST https://app.posteverywhere.ai/api/v1/posts \
-H "Authorization: Bearer pe_live_your_key" \
-H "Idempotency-Key: 6f1c2a9e-3b7d-4a51-9c0e-2d8f4b7a1e35" \
-H "Content-Type: application/json" \
-d '{"content": "Launching today", "account_ids": [1, 2]}'