# Errors and limits

> The Praeko API error envelope, the 116 contract error codes with status and meaning, and the limits for requests, bodies, media and webhooks.

Source: https://praeko.app/docs/en/guides/errors-and-limits

The HTTP status is the failure's status. The body always follows the same envelope; `code` is stable (English, snake_case) and `message` is Portuguese text.

**422**

```json
{
  "error": {
    "code": "time_in_past",
    "message": "O horário já passou ou está perto demais.",
    "details": {}
  }
}
```

- `401` comes with `WWW-Authenticate: Bearer`; `429` comes with `Retry-After` (seconds).
- With `422 invalid_field` and `invalid_request`, `details.errors` lists every problem at once.
- Portuguese field names are rejected: send the contract names (in English) and the `X-Praeko-Contract: en` header.

## Limits

| Limit | Value |
|---|---|
| Requests | 300 per minute per key (approximate, per instance). Over it: `429 rate_limited` with `Retry-After`. |
| JSON body | 1 MB (`413 body_too_large`) |
| Media file | Up to 50 MiB in one request (`simple: true`); above that, in 5 to 95 MiB parts. Video up to 5 GiB, cover and photo up to 20 MiB. |
| Webhooks | 10 per workspace; 10 s delivery timeout |
| Usage log | 30 days, metadata only (route, status, duration; never the body) |

## The codes (116)

Status and text come from the documented routes and, when a route does not list them, from the server's behavior. Only codes from internal platform operation routes are left out. A dash (—) in the status means the current server does not return that code.

| Status | `code` | Meaning |
|---|---|---|
| 404 | `route_not_found` | No route matches this method and path. |
| 405 | `method_not_allowed` | The path exists but does not accept this method. |
| 500 | `internal_error` | Unexpected server error. The `message` is generic; try again. |
| 401 | `unauthorized` | The `Authorization: Bearer …` header is missing. |
| 401 | `invalid_api_key` | The key does not exist. |
| 401 | `revoked_api_key` | The key has been revoked. |
| 401 | `expired_api_key` | The key has expired. |
| 401 | `mfa_required` | Confirm the second factor to continue. |
| 401 | `invalid_code` | Wrong code. |
| 409 | `mfa_already_enabled` | The authenticator app is already on: turn it off before setting it up again. |
| 409 | `mfa_not_started` | Start by setting up the authenticator app. |
| 409 | `mfa_not_enabled` | Turn on the authenticator app first. |
| 409 | `current_session` | This is the session you are using: sign out instead. |
| 404 | `session_not_found` | Session not found. |
| 422 | `invalid_confirmation` | The body is missing the confirmation this destructive action requires. |
| 401 | `invalid_session` | The panel session expired or is no longer valid: sign in again. |
| 403 | `invalid_origin` | A request with a panel session came from another origin and was rejected. |
| 403 | `forbidden` | The key's scope is not enough for this route (see the scope badge). |
| 429 | `rate_limited` | The per-minute request limit was exceeded. |
| 400 | `empty_body` | The request body is empty. |
| 400 | `invalid_json` | The request body is not a JSON object. |
| 413 | `body_too_large` | The request body is larger than 1 MB. |
| 422 | `invalid_field` | A body, query or header field has the wrong format or is out of range. The `message` says which one. |
| 422 | `invalid_request` | Something is missing; the full list comes in `details.errors`. |
| 422 | `missing_schedule` | Missing `scheduledFor`, `queue` or `draft`. |
| 422 | `missing_time` | Missing `scheduledFor`. |
| 422 | `time_in_past` | The time has already passed or is too close. |
| 409 | `no_slots` | `queue` on a channel without time slots. |
| 409 | `no_free_slot` | The channel has no free slot in the next 120 days. |
| 404 | `post_not_found` | The post does not exist. |
| 404 | `item_not_found` | The item does not exist in this post. |
| 404 | `action_not_found` | This item has no saved comment. |
| 404 | `channel_not_found` | The channel does not exist in this workspace, or the network is not connected. |
| 409 | `channel_not_connected` | The network is not connected. |
| 404 | `api_key_not_found` | The key does not exist. |
| 403 | `oauth_requires_panel` | Connecting an account happens in the panel (Channels › Connect), not with an API key. |
| 409 | `video_not_private` | YouTube, already uploaded: the video is already public on YouTube, so its time and metadata can only change in Studio (`details.studio` has the link). |
| 409 | `channel_in_use` | Pending items use this account: cancel or move them before removing it. |
| 409 | `channel_connected` | Disconnect the account before removing it from the list. |
| 409 | `channel_is_buffer` | This is the Instagram channel that publishes through Buffer: it cannot be removed. |
| 401 | `invalid_credentials` | Wrong e-mail or password. |
| 422 | `weak_password` | The password breaks the rules: 12 to 128 characters and nothing predictable (such as the e-mail or repeated characters). |
| 400 | `invalid_link` | Invalid or expired link: ask for a new one. |
| 429 | `account_locked` | Too many wrong attempts: the account is locked for a few minutes. |
| 401 | `invalid_passkey` | The passkey failed verification. |
| 404 | `passkey_not_found` | Passkey not found. |
| 409 | `passkey_exists` | This passkey is already registered. |
| 400 | `invalid_challenge` | Invalid or expired challenge: start again. |
| 409 | `passkey_limit` | The limit is 20 passkeys per account. |
| 401 | `code_required` | Send the authenticator app code (`code`). |
| 409 | `api_key_in_use` | It is the key making this call. |
| 401 | `invalid_token` | OAuth token is invalid, expired or issued for another server (on `/mcp`). |
| 404 | `request_not_found` | The agent authorization request does not exist or has expired: start again from the app. |
| 403 | `request_from_another_browser` | The authorization request was started in another browser: start the connection again from the app. |
| 404 | `connection_not_found` | Agent connection not found. |
| 409 | `video_not_found` | YouTube, already uploaded: the video was deleted on YouTube. |
| 404 | `no_video` | The post has no video on YouTube yet. |
| 404 | `no_avatar` | The channel has no profile picture available. |
| 404 | `media_not_found` | The media does not exist in this workspace. |
| 409 | `media_in_use` | A pending post uses this media item. |
| 409 | `media_not_ready` | The media upload has not been completed yet. |
| 422 | `media_type_mismatch` | The media is the wrong type (a video where a cover goes, or the reverse), or the cover is not JPEG or PNG. |
| 409 | `media_locked` | The video can only be replaced in an adopted post or in a draft; the image of an image post cannot be replaced by PATCH. |
| 404 | `unknown_network` | Unknown network. |
| 422 | `network_not_supported` | This action does not support that network yet. |
| 409 | `invalid_state` | The media is not waiting for this upload (it was created another way, or it was already sent). |
| 409 | `id_exists` | A post with this `id` already exists. |
| 409 | `already_adopted` | This Buffer post is already in Praeko. |
| 409 | `nothing_to_publish` | This post has no pending item (or none for the requested network). |
| 409 | `not_editable` | The item has already gone out or is in progress (YouTube, already uploaded: it can only change while the video is still private and scheduled; once the scheduled time has passed, it can no longer change). |
| 409 | `post_adopted` | Post adopted from Buffer: only text, time and draft can change. |
| 409 | `post_prepared` | This Reel is already being prepared on Meta: the caption can no longer change; you can only cancel. |
| 409 | `oauth_not_configured` | Connecting that network is not configured on this server. |
| 409 | `followers_from_api` | The network already provides followers through its API (only `instagram` accepts a manual value). |
| 409 | `youtube_not_connected` | YouTube is not connected. |
| 409 | `instagram_meta_not_connected` | The Meta API is not connected. |
| 422 | `download_failed` | The file could not be downloaded from the URL (redirect without a target, error status or empty body). |
| 422 | `unsupported_mime` | The file type is not accepted for that `type` (video: MP4 or MOV; cover: PNG or JPEG). |
| 422 | `file_too_large` | The file is over the limit of that upload path (for example, `simple: true` above 50 MiB). |
| 413 | `part_too_large` | The part is larger than 95 MiB. |
| 422 | `part_too_small` | A part (other than the last) is smaller than 5 MiB. |
| 422 | `part_empty` | The part is empty. |
| 422 | `parts_unequal` | The parts have different sizes: all must be equal, only the last can be smaller. |
| 422 | `parts_missing` | A part is missing from the sequence: parts go from 1 to N with no gaps. |
| 422 | `no_parts` | No part was uploaded. |
| 422 | `size_mismatch` | The total uploaded does not match the declared `size`. |
| 422 | `storage_rejected` | Storage refused to join the parts; upload the parts again. |
| — | `identity_not_linked` | Reserved in the contract; the current server does not return it. |
| — | `last_login_method` | Reserved in the contract; the current server does not return it. |
| 409 | `missing_oauth_credential` | The Meta connection is not configured on this server. |
| 409 | `already_connected` | Instagram is already connected; nothing changed. |
| 400 | `oauth_state_invalid` | Meta return page: the request is incomplete, expired or from another session. Start the connection again in the panel. |
| 400 | `oauth_denied` | Meta return page: authorization was denied. Nothing changed. |
| 422 | `no_instagram_account` | Meta return page: the sign-in gave access to no Instagram professional account. |
| 409 | `multiple_instagram_accounts` | Meta return page: the sign-in gives access to more than one account; pick one. |
| 502 | `token_exchange_failed` | Meta return page: Meta refused to exchange the code for a token. Nothing changed. |
| 422 | `publish_permission_missing` | Meta return page: the token lacks the publish permission. |
| 422 | `token_cannot_read_account` | Meta return page: the token cannot read the Instagram account. |
| 404 | `webhook_not_found` | The webhook does not exist. |
| 404 | `delivery_not_found` | The delivery does not exist. |
| 422 | `unsafe_url` | The url is not https, or points to a private network or localhost. |
| 409 | `webhook_limit_reached` | This workspace already has 10 webhooks. |
| 404 | `idea_not_found` | The idea does not exist. |
| 409 | `idea_limit_reached` | This workspace already has 2000 ideas. |
| 422 | `idempotency_key_reuse` | The `Idempotency-Key` was already used with a different request (different method, path, query or body). |
| 409 | `idempotency_in_progress` | A request with the same `Idempotency-Key` is still in progress (retry in a moment). |
| 409 | `idempotency_response_too_large` | The original response was larger than 256 KB and was not stored; fetch the resource with GET instead. |
| 422 | `invalid_image` | An `image` media item whose file is not a JPEG (checked on upload; for uploads in parts, on complete). |
| 422 | `image_aspect_ratio` | The image aspect ratio is outside 4:5 to 1.91:1 (Instagram would refuse it; use 1080x1350). |
| 422 | `image_too_large` | The image is over 8 MB (Instagram limit). |
| 422 | `kind_not_supported_by_network` | `kind: image` on a YouTube or TikTok item. |
| 422 | `carousel_not_supported` | More than one image in `media.images` (carousel arrives later). |
| 502 | `buffer_error` | Buffer refused to edit or cancel the post. |
| 502 | `instagram_error` | Meta (Instagram) rejected the request or did not respond. |
| 502 | `tiktok_unavailable` | The TikTok account could not be queried. |
| 502 | `youtube_error` | YouTube did not respond or rejected the check/write; nothing was changed (or `itemResults` says what was). |

The old Portuguese paths answer `{ "erro": { "codigo", "mensagem", "detalhes" } }` with the old code; the mapping is one to one.
