# Errors and status codes

One error shape for every endpoint, and what to do about each code.

> **In a nutshell:** Every error is {"error": {"code", "message"}}. Branch on error.code - it is stable - and show error.message to people. Validation errors also list the failing fields.

## The error envelope

```json
{"error": {"code": "outside_service_window", "message": "The customer has not messaged in the last 24 hours. Send an approved template instead."}}
```

Validation errors add the failing fields, keyed in dot notation:

```json
{
  "error": {"code": "validation_failed", "message": "The to field must be an international phone number (digits, optional leading +). (and 2 more errors)"},
  "errors": {
    "to": ["The to field must be an international phone number (digits, optional leading +)."],
    "text": ["The text field is required when type is text."],
    "text.body": ["The text.body field is required when type is text."]
  }
}
```

> [!NOTE]
> Messages may be reworded; codes will not change within `v1`. Never parse the message.

## Request IDs

Every response has an `X-Request-ID` header, and error bodies repeat it as
`error.request_id`. Send your own `X-Request-ID` (8-128 characters: letters,
digits, `.` `_` `:` `-`) to tie PaalChat's logs to yours; otherwise PaalChat
makes one. Log it with failed calls and quote it when you contact PaalTech
support - it identifies the request, and the send it queued, in PaalChat's logs.

## Codes

| HTTP | `error.code` | Meaning | Retry? |
|---|---|---|---|
| 401 | `unauthenticated` | Missing, wrong, revoked or expired token | No - fix the token |
| 403 | `missing_ability` | The token lacks the ability for this endpoint | No |
| 403 | `product_suspended` | Your product is suspended on PaalChat | No - contact PaalTech |
| 403 | `product_token_required` | Not a product token | No |
| 403 | `forbidden` | Not allowed for another reason | No |
| 404 | `not_found` | Business, WhatsApp account or message not found - or not yours | No |
| 404 | `unknown_phone_number` | `phone_number_id` is not connected for this business | No |
| 405 | `method_not_allowed` | Wrong HTTP method | No |
| 403 | `sandbox_only` | A simulation called with a live key | With a test key |
| 409 | `sandbox_not_connected` | The sandbox business has no connected (simulated) WhatsApp yet | After connecting it |
| 409 | `sandbox_not_sent` | Only sent outgoing messages get simulated statuses | No |
| 422 | `invalid_idempotency_key` | The Idempotency-Key header is not 1-255 printable characters | With a valid key |
| 409 | `idempotency_key_reused` | This Idempotency-Key was used for a different request | With a new key |
| 409 | `idempotency_in_progress` | The first request with this key is still running | Yes, shortly, same key |
| 429 | `business_limit_reached` | The business reached its daily message limit | Tomorrow |
| 422 | `recipient_suppressed` | The number is on the business's suppression list | After removing it |
| 422 | `recipient_opted_out` | The contact opted out of this category (or marketing needs an opt-in) | After they opt in |
| 422 | `recipient_preference_off` | The contact turned WhatsApp or this topic off | No |
| 403 | `urgent_not_allowed` | `urgent` needs the `messages.urgent` ability | With such a key, or without urgent |
| 422 | `invalid_template_parameters` | The template's variables, header media or button value are missing, extra or invalid (the message lists them) | With the right parameters |
| 409 | `not_cancellable` | The scheduled message is already being sent | No |
| 409 | `campaign_not_allowed` | The campaign cannot do that in its state, or its template is gone | After fixing it |
| 409 | `transfer_not_pending` | The account transfer is no longer waiting for an answer | No |
| 409 | `channel_not_enabled` | The business has not set up SMS or email | After Set up SMS or email |
| 403 | `account_not_verified` | A self-serve account sent a template before verifying its phone number in the dashboard | After verifying |
| 429 | `sending_limit_reached` | A new self-serve account reached its daily limit of business-initiated messages (it rises as the account builds a good record) | Tomorrow |
| 422 | `channel_not_allowed` | A sender ID, from address or provider PaalChat cannot use | With an allowed one |
| 422 | `invalid_provider_credentials` | Arkesel does not accept the business's own API key | With a working key |
| 503 | `provider_unavailable` | Arkesel could not be reached to check the business's own key | Shortly |
| 402 | `plan_limit_reached` | The business's PaalChat plan allows no more (members, contacts) | After upgrading the plan |
| 402 | `plan_feature_missing` | The plan does not include this (inbox, campaigns, automations) | After upgrading the plan |
| 503 | `payment_unavailable` | Online payment is not set up, or Paystack did not answer | Later |
| 409 | `business_suspended` | The business is suspended on PaalChat | No |
| 409 | `not_connected` | No connected WhatsApp number | After reconnecting |
| 409 | `connection_paused` | PaalChat operators paused sending for this WhatsApp account | Later, when resumed |
| 413 | `payload_too_large` | Request body too large | No - shorten it |
| 422 | `validation_failed` | Fields invalid - see `errors` | No - fix the request |
| 403 | `feature_disabled` | Media is not enabled for this business | No - ask PaalTech to enable it |
| 422 | `unknown_member` | No active inbox member with that external_id | After signing them in to the inbox |
| 404 | `unknown_media` | No such media for this business | After uploading the file |
| 409 | `media_not_ready` | The media is still being fetched, failed or was deleted | When `media.updated` says stored |
| 422 | `media_type_mismatch` | The file does not suit the message type (e.g. a PDF as image) | With the right type |
| 422 | `unsupported_media` | WhatsApp cannot send this kind of file | With a supported file |
| 422 | `media_too_large` | Over WhatsApp's size limit for the type | With a smaller file |
| 422 | `outside_service_window` | Free-form text outside 24 hours | Send a template |
| 422 | `unknown_template` | No such template in that language | No |
| 422 | `template_not_approved` | The template is not `APPROVED` | When approved |
| 422 | `phone_number_required` | Several numbers - pass `phone_number_id` | With the field |
| 422 | `template_rejected` | Meta refused the template (the message gives Meta's reason) | After fixing the template |
| 422 | `waba_required` | Several WhatsApp accounts - pass `waba_id` | With the field |
| 503 | `meta_unavailable` | Meta did not answer normally | Yes, later |
| 422 | `return_url_not_allowed` | The connect link's `return_url` origin is not registered for your product | After PaalTech registers it |
| 429 | `rate_limited` | Over 300 requests per minute | After `Retry-After` seconds |
| 500 | `server_error` | A PaalChat problem | Yes, with backoff |

## Delivery failures

A message accepted with `202` can still fail at Meta. That is not an HTTP error: it
arrives as a [`message.status`](/docs/api/callbacks/message-status) callback with
`status: failed` and `error: {code, message}`. Common codes:

| `error.code` | Meaning |
|---|---|
| `131026` | Undeliverable - the number is not on WhatsApp or uses an old app |
| `131047` | More than 24 hours since the customer's last message |
| `131049` | Meta limited marketing messages to this user |
| `131050` | The user stopped marketing messages |
| `132000` | Template parameter count does not match |
| `132001` | Template missing or not approved at Meta |
| `130429`, `131056` | Meta rate limits |
| `connection_unavailable` | The WhatsApp account was disconnected before the message was sent |
| `no_message_id` | Meta accepted the message but returned no ID |

Other numeric codes are Meta's [error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes).

## Handling strategy

- **4xx except 429**: do not retry; fix the cause or show the message.
- **429 and 5xx**: retry with backoff - see [Safe retries](/docs/safe-retries).
- **Network errors on a send**: retry with the **same** `reference`.
