Resources
Error codes
All the API's error codes, what they mean, and whether retrying is worthwhile.
Updated November 12, 2026View as Markdown
Every error has the shape { "error": { "code", "message", "details?", "requestId" } }. Base your code on code, never on the text of message. Keep the requestId: it is what lets us find the call.
Language of message: French by default. Send Accept-Language: en to receive it in English; the code never changes. Texts returned by Meta (for example the reason a template was rejected) are not translated.
Code table#
| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
validation_error | 400 | A field is invalid or missing; details lists it. | No, fix it first. |
unauthorized | 401 | Key or session missing, invalid, expired or revoked. | No, check the token. |
quota_exceeded | 402 | Your plan's monthly quota is reached (production). | No, change plan or wait for next month. |
plan_limit_reached | 402 | The plan does not allow more WhatsApp numbers (tier cap), nor, on the default plan, more connected businesses. | No: change plan. |
forbidden | 403 | The key lacks the required scope; the message names it. | No, create a key with the right scope. |
sandbox_only | 403 | Action restricted to sandbox keys. | No. |
not_found | 404 | The resource does not exist, or belongs to another account or environment. | No. |
conflict | 409 | Duplicate (name, external reference, WhatsApp account already connected…) or action not possible in the current state (delivery in progress, endpoint disabled). | Depends on the case. |
invalid_message | 422 | Invalid message: malformed number, empty text, ambiguous media, non-https link… | No. |
outside_service_window | 422 | Free-form message outside the 24-hour window. | No: send a template, or wait for the customer to write. |
template_not_approved | 422 | The template is not approved by Meta. | No, wait for approval. |
idempotency_conflict | 422 | Same Idempotency-Key for a different request. | No: use another key. |
meta_error | 502 | Meta rejected the request or is not responding; the message reflects its response. | Yes if it is an outage, no if it is a rejection. |
internal_error | 500 | Error on our side. | Yes, then contact support with the requestId. |
Real examples#
400 — validation_error#
{
"error": {
"code": "validation_error",
"message": "Invalid request: see details",
"details": [
"accountName must be longer than or equal to 2 characters",
"email must be an email"
],
"requestId": "req_9333db71-3a23-4154-8cd4-3dfc99f1001a"
}
}details indicates the field and the rule that was violated.
401 — unauthorized#
{
"error": {
"code": "unauthorized",
"message": "API key required (Authorization: Bearer pk_…)",
"requestId": "req_1c1b9300-f2d4-4387-88b8-7d66222d7497"
}
}403 — forbidden#
{
"error": {
"code": "forbidden",
"message": "Missing scope on this key: connections:read",
"requestId": "req_5acc8bd1-f598-4030-8ccd-6eb02dcfadc8"
}
}404 — not_found#
{
"error": {
"code": "not_found",
"message": "Message not found",
"requestId": "req_9b02107e-cfba-4b78-81c0-a0fd32068164"
}
}409 — conflict#
{
"error": {
"code": "conflict",
"message": "An app with this name already exists",
"requestId": "req_f4a2e5dd-8d8c-4e03-881a-024492da7967"
}
}422 — outside_service_window#
{
"error": {
"code": "outside_service_window",
"message": "This contact has not written to you in the last 24 hours: only an approved template can be sent to them",
"requestId": "req_4fdba006-98d4-4164-893d-98389fb54c62"
}
}422 — invalid_message#
{
"error": {
"code": "invalid_message",
"message": "Invalid recipient number: use the international format with a \"+\", e.g. +237600000000",
"requestId": "req_72871759-6841-4dcf-8065-3425c4a44633"
}
}422 — idempotency_conflict#
{
"error": {
"code": "idempotency_conflict",
"message": "This idempotency key has already been used for a different request",
"requestId": "req_af73c415-1365-41e6-801f-5fc18b1ee2ec"
}
}Message delivery errors#
Once the message has been accepted (202), failures no longer appear in an HTTP response: the message moves to failed, with an error that carries Meta's code and message, and a message.failed event is sent to your webhooks.
| Situation | Automatic retry? |
|---|---|
| Meta outage, timeout, rate limit | Yes, up to 6 attempts with increasing backoff |
| Permanent rejection (template not found, recipient unreachable, content rejected) | No: failed immediately |
The numeric codes that appear in error.code are Meta's: see its documentation for their exact meaning.