Build and operate
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
{"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:
{
"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 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.
Handling strategy
- 4xx except 429: do not retry; fix the cause or show the message.
- 429 and 5xx: retry with backoff - see Safe retries.
- Network errors on a send: retry with the same
reference.