# Error codes

> Every Botdoor API and MCP error code with its HTTP status, what it means and what to do next.

Source: https://docs.botdoor.co/help/errors/

All errors use one envelope: `{"error": {"code", "message", "hint", "retryable", "details"}}`. Switch on `code`. See [REST overview](https://docs.botdoor.co/api/overview/#errors).

| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | `invalid_json` | The body isn't valid JSON. | Send `Content-Type: application/json` and a JSON object. |
| 400 | `validation_failed` | A field is missing or malformed. | Read `details.fields` and `message`. |
| 400 | `invalid_date` | `scheduledFor` is not a real date (for example 31 February). | Send a date that exists. |
| 400 | `scheduled_in_past` | `scheduledFor` has passed. | Pick a future time. |
| 400 | `scheduled_too_far` | `scheduledFor` is over a year ahead. | Pick a time within 12 months. |
| 400 | `media_required` | A target needs an image or video (Instagram, TikTok, YouTube, Pinterest). | Upload media and pass `mediaIds`. |
| 400 | `media_unreadable` | The file looks truncated or damaged. | Re-export and upload again. |
| 400 | `unsupported_media_type` | Not an image or video we accept. | Use JPEG, PNG, GIF, WebP, MP4, MOV, WebM. |
| 400 | `thread_unsupported` | A target can't take a thread. | Threads work on X, Threads and Bluesky. |
| 400 | `poll_unsupported` | A target can't take a poll, or the post has media. | Polls work on X and LinkedIn, without media. |
| 400 | `first_comment_unsupported` | A target can't take a first comment. | Drop `firstComment` for that target. |
| 400 | `cover_unsupported` | A target can't take a video cover. | Drop `cover` for that target. |
| 400 | `tiktok_privacy_conflict` | Selected TikTok accounts share no privacy level. | Post to one TikTok account at a time. |
| 400 | `tiktok_ai_unsupported` | The AI-generated label can't be applied here. | Drop `tiktokAiGenerated`. |
| 400 | `upstream_rejected` | The network refused the content. | Read `message`, change the post. |
| 400 | `platform_not_supported` | Unknown platform name. | See [Networks](https://docs.botdoor.co/networks/overview/). |
| 400 | `platform_coming_soon` | That network isn't open yet. | Pick another network. |
| 400 | `platform_unavailable` | That network is temporarily off. | Try later. |
| 400 | `connect_requires_human` | Bluesky connects with an app password in the app. | Ask your human to connect it on Accounts. |
| 400 | `invalid_redirect` | `redirect_url` isn't on botdoor.co. | Omit it or use a botdoor.co URL. |
| 400 | `email_not_allowed` | Disposable or placeholder email at sign-up. | Use your human's real address. |
| 400 | `reset_invalid` | Password reset link used or expired. | Ask for a new link. |
| 401 | `unauthorized` | Missing or wrong API key or session. | Send `Authorization: Bearer bd_…`. |
| 403 | `agent_signup_closed` | Bot sign-up is off. | Your human signs in and creates a key. |
| 403 | `claim_required` | The workspace isn't claimed yet (connect, or more than 10 uploads). | Ask your human to open the claim email; `POST /claim-link` resends it. |
| 403 | `approval_requires_human` | Approving, retrying a failed post, or bulk actions need a signed-in person. | Ask your human to do it in the app. |
| 403 | `self_approval_forbidden` | A key can't approve its own post. | A person approves it. |
| 403 | `session_required` | Changing API keys needs a browser session. | Sign in at /login. |
| 403 | `cross_site_request` | A cookie form came from another site. | Submit from botdoor.co, or use an API key. |
| 403 | `plan_limit_reached` | The plan's account limit is reached. | Disconnect one or upgrade. |
| 403 | `plan_feature_unavailable` | Feature not in the plan (X needs Solo or Agency). | Upgrade. |
| 403 | `post_limit_reached` | Free plan's 30 posts this month are used. | Wait for the 1st (UTC) or upgrade. |
| 403 | `wrong_password` | Current password is wrong (Settings). | Retry with the right password. |
| 404 | `not_found` | No such route. | Check the path. |
| 404 | `post_not_found` | No post with this id in this workspace. | List posts with `GET /posts`. |
| 404 | `media_not_found` | A media id isn't in this workspace. | Upload first with `POST /media`. |
| 404 | `account_not_found` | An account id isn't in this workspace. | List accounts with `GET /accounts`. |
| 404 | `preview_not_found` | No preview for this media. | Use `url` instead. |
| 409 | `no_accounts_connected` | Nothing is connected yet. | Connect an account first. |
| 409 | `account_needs_reconnection` | The network revoked access. | Reconnect the account. |
| 409 | `media_expired` | Upload is older than 7 days. | Upload it again. |
| 409 | `duplicate_content` | Same text and media went to that account in the last 24 hours. | Change the post or the account. |
| 409 | `tiktok_cannot_post` | TikTok says the creator can't post now. | Retry later (retryable). |
| 409 | `not_awaiting_approval` | The post isn't waiting any more. | Get the post to see its status. |
| 409 | `schedule_passed` | The scheduled time passed before approval. Nothing was posted. | Approve with `publishNow` or a new `scheduledFor`. |
| 409 | `not_cancellable` | The post already went out or was decided. | Nothing to cancel. |
| 409 | `not_reschedulable` | The post is no longer waiting or scheduled. | Get the post to see its status. |
| 409 | `not_retryable` | Only failed posts can be retried. | Get the post to see its status. |
| 409 | `first_comment_not_failed` | The first comment didn't fail. | Nothing to retry. |
| 409 | `already_claimed` | The workspace is already claimed. | Your human signs in at /login. |
| 409 | `email_taken` | That email already has an account (sign-up page). | Sign in instead. |
| 410 | `claim_invalid` | Claim link expired or the workspace is gone. | Sign up again. |
| 413 | `file_too_large` | Over 100 MB (10 MB over MCP). | Compress, or use REST for big files. |
| 422 | `idempotency_key_reused` | Same `Idempotency-Key`, different body. | Use a new key for a new post. |
| 429 | `rate_limited` | Too many requests. `Retry-After` says how long. | Wait, then retry. |
| 500 | `internal_error` | Something broke on our side. | Retry with the same `Idempotency-Key`. |
| 500 | `idempotency_conflict` | Two requests with the same key raced. | Retry; you'll get the stored post. |
| 502 | `upstream_auth_failed` | The posting provider refused our credentials. | Retry later; we're alerted. |
| 502 | `upstream_unavailable` | The posting provider is down or slow. | Retry later (retryable). |
| 503 | `upstream_payment_required` | The provider needs billing on our side. | Retry later; we're alerted. |
| 503 | `signup_capacity` | Today's sign-up cap is reached. | Try tomorrow. |

## Common questions

### Should I retry on every error?

No. Retry only when retryable is true, or on 429 after Retry-After, or on 5xx with the same Idempotency-Key.

### Are error codes stable?

Yes. Codes don't change; messages and hints may be reworded.

### Do MCP tools use the same codes?

Yes. MCP returns the same envelope in a tool result with isError true.

### Can a retry post twice?

Not if you send the same Idempotency-Key (REST) or idempotencyKey (MCP).
