Documentación / Completo
El catálogo completo de errores
¿Programás con IA? Conectala a tu cuenta por MCP: revisa tu configuración, crea webhooks y plantillas con tu confirmación y te escribe el código. Ver cómo →
Todos los code que emite /v1, con su HTTP y qué hacer. Son estables: desde el primer integrador NO se renombran (romperían tu switch). Inglés solamente, a propósito (el code es el contrato, el message es para pegar en un log y buscar en Google).
⚠ Hay dos catálogos distintos. Este son los errores HTTP (el sobre { error: { code } }, la respuesta inmediata). Un mensaje que WhatsApp rechaza DESPUÉS del 202 no aparece acá: su motivo viaja en lastErrorCode cuando consultás el estado del mensaje (WINDOW_24H_EXPIRED, RECIPIENT_OPTED_OUT, …). Están en el openapi bajo Message failure codes.
🔴 Cuenta bloqueada por cobro: 402, no lo reintentes. Con la prueba vencida, /v1 contesta 402 TRIAL_EXPIRED; con un pago vencido, 402 BILLING_PAST_DUE. Sin Retry-After: reintentar no sirve. El cuerpo trae hasta cuándo seguís recibiendo (details.receivingUntil), el enlace a Cobro (details.billingUrl) y el mensaje en tu idioma (details.localizedMessage). Frená tu cola y elegí un plan; el envío vuelve en el momento. Hasta el 19/10/2026 era 403 ACCOUNT_SUSPENDED, que hoy queda sólo para una suspensión manual o una baja.
| code | HTTP | message | acción |
|---|---|---|---|
API_KEY_MISSING | 401 | The Authorization header is missing or malformed | Bug in your integration — send Authorization: Bearer wc_live_… |
API_KEY_INVALID | 401 | The API key is invalid or has been revoked | Check / rotate the key in the panel |
TRIAL_EXPIRED | 402 | Your free trial ended on <date>. Sending is paused; receiving continues until <date>. Add a plan: <link> (details: trialEndedAt, receivingUntil, receivingStopped, billingUrl, localizedMessage). No Retry-After | Do NOT retry: stop your queue and pick a plan in Billing (details.billingUrl); sending comes back right away |
BILLING_PAST_DUE | 402 | Your payment is overdue and sending is paused since <date>; receiving continues until <date>. Pay your plan: <link> (details: sendingPausedAt, receivingUntil, receivingStopped, billingUrl, localizedMessage). No Retry-After | Do NOT retry: pay in Billing (details.billingUrl); sending comes back right away |
ACCOUNT_SUSPENDED | 403 | Account is suspended or cancelled (a manual suspension or a cancelled account — paying does not restore it) | Contact support |
RATE_LIMITED | 429 | A rate limit was exceeded: error.limit says which (connection, account, key) | Wait Retry-After (= error.retryAfterSeconds) and retry with the same Idempotency-Key; see Rate limits |
IDEMPOTENCY_KEY_REQUIRED | 400 | The Idempotency-Key header is required | Send a unique UUID in Idempotency-Key |
INVALID_TYPE | 400 | type must be text, template, image, document, audio or video | Fix type |
INVALID_BODY | 400 | text.body (or template.name) is required | Fix the message body |
INVALID_RECIPIENT | 400 | to must be a plausible E.164 number (8–15 digits) | Fix to |
MEDIA_LINK_OR_ID_REQUIRED | 400 | A <type>.link (https URL) or <type>.id (Meta media id) is required | Send one of them under the media object |
MEDIA_LINK_AND_ID | 400 | Provide EITHER link or id, not both | Send only one |
MEDIA_LINK_INVALID | 400 | <type>.link must be a well-formed https URL | Use a public https URL (Meta fetches it) |
AUDIO_CAPTION_NOT_ALLOWED | 400 | type audio does not accept a caption | Remove audio.caption |
STICKER_CAPTION_NOT_ALLOWED | 400 | type sticker does not accept a caption | Remove sticker.caption |
TEMPLATE_SEND_INVALID | 400 | The header/body/buttons shorthand has mistakes; all of them are in details.errors[] (field = JSON path, code, message in your language). Nothing was sent to Meta | Fix each field. GET /v1/templates/{id} shows the approved template (how many {{n}}, which buttons) |
CONNECTION_REQUIRED | 400 | Provide connectionId (conn_…) or from (phoneNumberId) | Send one of them |
CONNECTION_NOT_FOUND | 404 | No connection matches connectionId/from (or the {id} of /v1/connections/{id}: conn_… or externalId) for this account | Check the connection id (a number of ANOTHER account is also reported as not found) |
CONNECTION_NOT_READY | 409 | The number exists but is not connected yet (details.status = its real state) | Wait for it to become connected, or refresh its status in the panel — do NOT change your payload |
CONNECTION_BANNED | 409 | WhatsApp (Meta) blocked this number (details.status = banned). Not queued | Retrying does not help: the business appeals the block from its own Meta account (WhatsApp Manager). When Meta lifts it the number goes back to connected by itself (connection.status_changed) |
MESSAGE_NOT_FOUND | 404 | No message with that id for this account | Check the msg_… id |
INVALID_MESSAGE_ID | 400 | messageId is not a WhatsApp message id (a wamid.…) — e.g. a msg_… of a message you sent | Send data.message.id of message.received as messageId in the body |
INVALID_TYPING | 400 | typing must be true, false or omitted | Fix the body |
INBOUND_MESSAGE_NOT_FOUND | 404 | No received message with that id for this account (unknown, of another account, or older than 30 days) | Check the id; a message of ANOTHER account is also reported as not found |
MESSAGE_NOT_INBOUND | 409 | The id is a message sent FROM your number (by the API or the phone) | Only received messages can be marked as read |
MESSAGE_TOO_OLD | 410 | The message was received more than 30 days ago — Meta does not mark it | Nothing to do; mark messages when you read them |
MESSAGE_READ_REJECTED | 422 | Meta did not accept the read (details.metaCode) | Check the id came from this number's message.received; do not retry unchanged |
META_UNAVAILABLE | 502 | Meta did not answer (network or 5xx) | Retry in a few seconds |
QUOTA_EXCEEDED | 429 | Plan message quota exceeded (policy block) | Upgrade plan or wait for the period to renew |
MEDIA_ID_REQUIRED | 400 | A media id is required | Include the media id in the path |
MEDIA_NOT_FOUND | 404 | No media with that id for this account (or expired at Meta) | Only fetch media ids from your own inbound events |
MEDIA_TOO_LARGE | 413 | Media exceeds the size limit | Do not proxy files above the limit |
MEDIA_UPSTREAM | 502 | Could not resolve/download the media from Meta | Retry; the media may have expired |
TEMPLATE_NOT_APPROVED | 422 | The template you tried to send is known and NOT APPROVED (details.status = its state) | Only send approved templates; check status in the panel or wait for Meta |
TEMPLATE_HEADER_MEDIA_REQUIRED | 422 | The template has an IMAGE/DOCUMENT/VIDEO header and the send carries no header component. Caught before Meta (which answers an opaque 132000). details.example is the exact component to add | Add the header component with the file — link (public https) or id |
TEMPLATE_PARAM_INVALID_CHARS | 400 | A text parameter (body or header, type: text) contains a line break (\n), a carriage return (\r), a tab (\t) or 5+ consecutive spaces. Caught before Meta (which answers 132018). details = component, parameter ({{n}}), character — never the value. ⚠ Enabled per deployment (TEMPLATE_PARAM_GUARD_ENABLED); off, the send goes to Meta as before | Replace line breaks with · or commas and collapse repeated spaces |
TEMPLATE_HEADER_MEDIA_NOT_SUPPORTED | 422 | The send carries a media header parameter but the template has no media header | Drop the header component (a TEXT header takes text parameters, not a file) |
TEMPLATE_HEADER_MEDIA_TYPE_MISMATCH | 422 | The header parameter type doesn't match the approved header format (details.expectedParameterType) | Use the type the template declares — not the file's mime. A JPG on a DOCUMENT-header template still goes as document |
TEMPLATE_CAROUSEL_MISMATCH | 422 | Raw components don't match the approved template's carousel: a carousel template needs a carousel component with exactly its number of cards, and a non-carousel template can't carry one (details.expectedCards, details.sentCards). Since 0.24.0 | Send one card per card of the template, in order (card_index 0, 1, …), or use the template.carousel.cards shorthand, which checks everything |
TEMPLATE_PRECHECK_FAILED | 400 | The template would be rejected by Meta (details.issues[] lists each blocker + how to fix; since 0.22.0 also details.errors[] = field (JSON path, e.g. buttons[1].url), code, message in your language) | Fix the issues and retry — the pre-checker caught them before Meta did |
TEMPLATE_META_REJECTED | 400 | Meta rejected the template at creation or edit (message = Meta's reason) | Adjust per Meta's message and try again |
TEMPLATE_NOT_EDITABLE | 409 | Meta only allows editing APPROVED, REJECTED or PAUSED templates (details.status) | Wait for Meta's review, or create a new template |
TEMPLATE_FIELD_NOT_EDITABLE | 400 | name or language can't change on an edit (details.field) | Create a new template for another name or language |
TEMPLATE_CATEGORY_LOCKED | 409 | Meta doesn't allow changing the category of an approved template | Send the same category, or create a new template |
TEMPLATE_EDIT_LIMIT | 429 | An approved template can be edited once per 24 h and 10 times per 30 days (details.window, details.retryAt) | Retry after retryAt |
TEMPLATE_NOT_IN_META | 409 | The template has no Meta id yet (draft or never synced) | POST /v1/templates/sync and retry |
TEMPLATE_SAMPLE_TYPE_UNSUPPORTED | 415 | The sample file isn't a PDF, JPG/PNG or MP4 (details.detected) — the only types Meta's upload accepts | Convert the file and upload again |
TEMPLATE_SAMPLE_UPLOAD_FAILED | 502 | Meta didn't accept the sample (details.metaCode if Meta gave one). Nothing was stored | Retry |
MEDIA_FILE_REQUIRED | 400 | The upload carries no file (file part or base64 file) | Send the file |
MEDIA_TYPE_MISMATCH | 422 | The type you asked for doesn't match the file's bytes | Drop type or send the right file |
MEDIA_BUSY | 503 | Uploads are momentarily busy | Retry in a few seconds |
TEMPLATE_NOT_FOUND | 404 | No template with that id for this account | Check the tpl_… id |
TEMPLATE_NAME_TAKEN | 409 | A template with that name+language already exists on this number | Use a different name (Meta forbids duplicate names) |
TEMPLATE_CATEGORY_INVALID | 400 | category must be UTILITY or MARKETING (draft approve/edit). AUTHENTICATION is not creatable — Meta wants a shape this builder doesn't produce | Send UTILITY or MARKETING; for an auth template, create it in Meta Business Manager and sync it |
TEMPLATE_RUBRO_INVALID | 400 | suggestedRubro is not a known catalog rubro | Omit it or use a valid rubro |
TEMPLATE_DRAFT_LIMIT_INVALID | 400 | draftLimit must be an integer between 1 and 100 | Send a value in range or omit it |
TEMPLATE_DRAFT_NOT_FOUND | 404 | No pending draft with that id for this account | Check the tpl_… id |
TEMPLATE_LINK_INVALID | 404/410 | The template link is not valid (or was already consumed) | Generate a new template link |
TEMPLATE_LINK_EXPIRED | 410 | The template link has expired | Generate a new template link |
TEMPLATE_LINK_REVOKED | 410 | The template link was revoked | Generate a new template link |
TEMPLATE_LINK_DRAFT_LIMIT_REACHED | 429 | This link has reached its allowed number of proposals | Generate a new link, or raise draftLimit |
REQUEST_BODY_TOO_LARGE | 413 | The request body is larger than the limit (1 MB; 5 MB on /webhook) | Do not put a file inside the JSON. WAIA Connect stores no files: media travels by link and Meta downloads it. Host the file on a public https URL and send {"type":"document","document":{"link":"https://…"}}. ⚠ A Google Drive or Dropbox share URL returns an HTML page, not the file — Meta rejects it (MEDIA_DOWNLOAD_FAILED). If the body really is a large JSON, split the request |
REQUEST_BODY_INVALID_JSON | 400 | The request body is not valid JSON | Check for a trailing comma, single quotes instead of double, or an empty body sent with Content-Type: application/json |
REQUEST_BODY_UNSUPPORTED_ENCODING | 415 | The body uses an encoding or charset we cannot read | Send UTF-8 JSON with Content-Type: application/json |
INTERNAL | 500 | Internal server error | Retry; if it persists, contact support |
APIKEY_NAME_REQUIRED | 400 | A non-empty name is required to create an API key | Send a name |
APIKEY_NOT_FOUND | 404 | No API key with that id for this account | Check the key id |
WEBHOOK_URL_REQUIRED | 400 | A url string is required | Send a url |
WEBHOOK_URL_UNSAFE | 400 | URL must be https to a public host (private/loopback/link-local rejected) | Use a public https URL |
WEBHOOK_EVENTS_INVALID | 400 | events must be known event types | Use the 26 subscribable types of DeliveryEventType (message.received … onboarding.help_requested). webhook.test is not subscribable |
WEBHOOK_NOT_FOUND | 404 | No webhook endpoint with that id for this account | Check the wh_… id |
WEBHOOK_STATIC_HEADER_INVALID | 400 | The static header name is invalid or reserved | Use a valid header name; never Authorization/Cookie/Host/Content-* |
WEBHOOK_CREATE_FAILED | 500 | Could not create the webhook endpoint | Retry |
WEBHOOK_LIMIT_REACHED | 422 | No room for one more webhook there (details.scope: connection = 2 per number, connections_total = 2 × your numbers, account = 5 account-wide; details.used, details.limit) | Delete or reuse one (PATCH its url) — webhooks you already have above the limit keep working |
ALERT_NOT_FOUND | 404 | No alert with that id for this account | Check the alert id |
CONNECTION_QUOTA_EXCEEDED | 429 | This number's monthly message limit is exhausted | Raise the limit for THIS connection (≠ upgrading the plan) |
CONNECTION_EXTERNAL_ID_TAKEN | 409 | Another connection in this account already uses that externalId | Pick a different externalId (unique within an account) |
CONNECTION_LIMIT_INVALID | 400 | monthlyMessageLimit must be a non-negative integer or null | Fix the value |
CONNECTION_EMAIL_NOTICES_INVALID | 400 | emailNotices on a connection must be inherit, on or off. Since 0.36.0 | Send one of the three |
SETTINGS_INVALID | 400 | emailNotices on PATCH /v1/account/settings must be a boolean. Since 0.36.0 | Send true or false |
NOTICES_QUERY_INVALID | 400 | A GET /v1/notices parameter is invalid (limit 1–100, before a positive integer, unread true/false, lang es/en/pt/it, connectionId a string). Since 0.36.0 | Fix the parameter |
NOTICE_NOT_FOUND | 404 | No notice with that id for this account. Since 0.36.0 | Check the id (GET /v1/notices) — a notice of ANOTHER account is also reported as not found |
INVALID_DATE | 400 | from/to must be YYYY-MM-DD | Fix the date |
INVALID_PERIOD | 400 | period must be YYYY-MM | Fix the period |
WEBHOOK_CONNECTION_INVALID | 400 | connectionId is not a valid connection id (on /v1/webhooks: not a conn_…/externalId string, or both filters at once) | On /v1, send conn_… or your externalId (omit for the account default); on the panel surface, the numeric id |
WEBHOOK_CONNECTION_NOT_FOUND | 404 | No connection with that id (or externalId) for this account | Check the connection id — a number of ANOTHER account is also reported as not found |
ONBOARDING_MODE_INVALID | 400 | mode must be cloud_api, coexistence, or omitted | Fix mode |
ONBOARDING_ATTEMPT_STATUS_INVALID | 400 | Unsupported signup-attempt status (abandoned is written by the server only) | Send one of popup_returned, preflight_ok, completed, cancelled, failed |
ONBOARDING_LANG_INVALID | 400 | lang must be es, it, en, pt, or omitted | Fix lang |
ONBOARDING_EXPIRY_INVALID | 400 | expiresIn/expiresAt must be a single value between 5 minutes and 30 days | Send one of them, in range |
ONBOARDING_RETURN_ORIGIN_INVALID | 400 | returnOrigin must be an exact https origin (public, no path) | Send https://host (SSRF-guarded) |
ONBOARDING_REDIRECT_URL_INVALID | 400 | redirectUrl must be a public https URL | Send an https URL (SSRF-guarded) |
ONBOARDING_LINK_NOT_FOUND | 404 | No onboarding link with that id | Check the onb_… id |
ONBOARDING_CASE_STATUS_INVALID | 400 | status must be open, acked, all or omitted | Fix status |
ONBOARDING_CASE_NOT_FOUND | 404 | No onboarding case with that id | Check the A-<n> id (GET /v1/onboarding-cases) |
ONBOARDING_LINK_INVALID | 404 | This onboarding link is not valid | The token is wrong/unknown |
ONBOARDING_LINK_USED | 410 | The link was already used | Issue a new link |
ONBOARDING_LINK_EXPIRED | 410 | The link expired | Issue a new link |
ONBOARDING_LINK_REVOKED | 410 | The link was revoked | Issue a new link |
ONBOARDING_LINK_ALREADY_USED | 410 | This onboarding link was already used | Issue a new link (single use) |
ONBOARDING_HISTORY_INVALID | 400 | historyDelivery must be true, false, or omitted | Send a boolean, or omit it so the hosted page asks |
ONBOARDING_PREVIOUS_PROVIDER_INVALID | 400 | previousProvider must be 360dialog, twilio, gupshup, wati, other, none, unknown, or omitted | Send one of the seven, or omit it so the hosted page asks |
ONBOARDING_INTRO_INVALID | 400 | intro / linkIntro must be full, minimal, or null | Send one of the two, or null to inherit |
ONBOARDING_SIGNUP_HELP_INVALID | 400 | signupHelp must be off, brand, neutral, or null | Send one of the three, or null to inherit |
HISTORY_LIMIT_INVALID | 400 | limit must be an integer between 1 and 5 | Fix limit (default 1) |
HISTORY_CURSOR_INVALID | 400 | after is not a cursor this endpoint returned | Send the nextCursor of the previous page, or omit it to start over |
HISTORY_BATCH_NOT_FOUND | 404 | No history batch with that id for this connection | Check the hb_… id (it comes in data[].batchId) |
HISTORY_BATCH_GONE | 410 | The batch is no longer available: it expired after 7 days, or it could never be stored | Nothing to confirm; history.completed tells you how many expired |
BRAND_LOGO_URL_INVALID | 400 | brandLogoUrl must be a public https URL | Use a public https image URL (SSRF-guarded) |
BRAND_COLOR_INVALID | 400 | brandColor must be a #RRGGBB hex color | Fix the color |
BRAND_EMAIL_INVALID | 400 | brandSupportEmail must be a valid email | Fix the email |
BRAND_WEBSITE_INVALID | 400 | brandWebsite must be an https URL | Fix the URL |