Every code /v1 emits, with its HTTP status and what to do. They are stable: from the first integrator they are NOT renamed (that would break your switch). English only, on purpose (the code is the contract, the message is for pasting into a log and googling).
⚠ There are two separate catalogs. These are the HTTP errors (the { error: { code } } envelope, the immediate response). A message WhatsApp rejects AFTER the 202 does not show up here: its reason travels in lastErrorCode when you check the message status (WINDOW_24H_EXPIRED, RECIPIENT_OPTED_OUT, …). They live in the openapi under Message failure codes.
| code | HTTP | message | action |
|---|---|---|---|
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 |