# El catálogo completo de errores

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_.

| 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 |
| `ACCOUNT_SUSPENDED` | 403 | Account is suspended or cancelled | Resolve billing / contact support |
| `RATE_LIMITED` | 429 | Per-key rate limit exceeded | Back off and retry (limit 600/min per key) |
| `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 |
| `CONNECTION_REQUIRED` | 400 | Provide connectionId (conn_…) or from (phoneNumberId) | Send one of them |
| `CONNECTION_NOT_FOUND` | 404 | No connection matches connectionId/from 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 |
| `MESSAGE_NOT_FOUND` | 404 | No message with that id for this account | Check the msg_… id |
| `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_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_PRECHECK_FAILED` | 400 | The template would be rejected by Meta (details.issues[] lists each blocker + how to fix) | Fix the issues and retry — the pre-checker caught them before Meta did |
| `TEMPLATE_META_REJECTED` | 400 | Meta rejected the template at creation (message = Meta's reason) | Adjust per Meta's message and create again (an approved template can't be edited) |
| `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 message.received / message.status / message.echo / connection.status_changed / connection.created / usage.threshold_reached / connection.usage_threshold_reached / history.synced / template.status_changed |
| `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 |
| `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 |
| `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 | Send a numeric connection id or omit for the account default |
| `WEBHOOK_CONNECTION_NOT_FOUND` | 404 | No connection with that id for this account | Check the connection id |
| `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_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) |
| `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 |
