Errors
When a request fails, the API returns an HTTP status of 400 or higher and a JSON body with
"success": false, a code and a message. See Requests and responses
for the full shape.
{ "success": false, "code": "api_scope_insufficient", "message": "API key scope 'read' is insufficient for this endpoint (requires 'send')."}Use code in your code, not message. Messages can change. Codes do not change within v1.
New codes can be added; handle an unknown code by its HTTP status.
All error codes
Section titled “All error codes”This table lists every code the API returns today. Codes are case-sensitive: most are lowercase, and some older codes on the send endpoints are uppercase.
| Code | HTTP | What it means | What to do |
|---|---|---|---|
api_key_missingAll |
401 | The Authorization header is missing, does not start with Bearer , or has no key. |
Send the header exactly as shown in Authentication. |
api_key_invalidAll |
401 | The key is not recognised: it does not start with pk_live_, it is wrong, or it was revoked or rotated. |
Check that you send the full key. If it was revoked or rotated, use the new key. |
api_key_ip_blockedAll |
403 | The key has an IP allowlist, and the request came from an address that is not on it. | Call from an allowed server, or create a key with the right allowlist. |
api_scope_insufficientAll |
403 | The key’s scope does not allow this endpoint. The message says which scope is needed. | Use a key with the right scope. See Scopes. |
private_auth_restrictedAll |
403 | This key is reserved for an internal SanadWA feature. This happens when a key’s name starts with private-auth:. |
Create a new key whose name does not start with private-auth:. |
api_key_lookup_failedAll |
500 | SanadWA could not check the key because of a problem on our side. | Retry after a few seconds. |
rate_limitedAll |
429 | Too many requests with this key, or more than 5 campaign creations in a minute. Nothing was processed. | Wait for the seconds in Retry-After. See Rate limits. |
not_foundAny path |
404 | The URL is not an endpoint, or the HTTP method is wrong for it. | Check the method, the path and the base URL https://api.sanadwa.com/v1. |
INVALID_JSONJSON bodies |
400 | The body is not valid JSON. | Fix the JSON. |
PAYLOAD_TOO_LARGEJSON bodies |
413 | The body is larger than 100 KB (1 MB for POST /contacts). |
Send a smaller body. |
validation_errorWrites |
400 | A field or parameter is missing, has the wrong type or is out of range. The message names it. | Fix the field. Check Content-Type: application/json. |
idempotency_key_too_longWrites |
400 | The Idempotency-Key header is longer than 120 characters. |
Use a shorter key, for example a UUID. |
idempotency_in_progressWrites |
409 | A request with the same Idempotency-Key is still running. |
Wait for Retry-After and retry with the same key. |
idempotency_conflictWrites |
409 | The Idempotency-Key was already used, successfully, for a different request: another body, or on the contacts, tags and campaigns endpoints another method or path. Nothing was done. |
Use a new key for a new request. See Idempotency. |
phone_not_foundSends, GET /templates, POST /campaigns |
404 | The phone_number_id is not an active number in your account. |
Get valid ids from GET /phone-numbers. |
account_not_foundSends |
404 | The WhatsApp Business Account of this number is no longer linked to your SanadWA account. | Reconnect WhatsApp in the console. |
PREPAID_CREDIT_CAP_REACHEDSends |
402 | Your account does not have enough credits for this message. details has needed, remaining and shortfall. Nothing was sent. |
Buy credits in the console, then retry. |
SUBSCRIPTION_LAPSEDSends |
402 | Your subscription ended recently and there are not enough credits. Nothing was sent. | Renew the subscription or buy credits, then retry. |
PREPAID_CAP_CHECK_FAILEDSends |
402 | SanadWA could not check your balance. Nothing was sent. | Retry after a few seconds. |
send_failedSends |
Varies | WhatsApp refused the message, or SanadWA could not finish the send. See send_failed below. | Read message, upstream_code and meta_error. |
TEMPLATE_CATEGORY_UNAVAILABLETemplate send |
503 | SanadWA could not look up the template’s category. Nothing was sent. | Retry after a few seconds. |
TRACKED_TEMPLATE_CAMPAIGN_ONLYTemplate send |
400 | The template has click-tracked buttons. Such templates can only be sent in campaigns. | Use another template, or send it with POST /campaigns. |
TRACKED_TEMPLATE_GUARD_UNAVAILABLETemplate send |
503 | SanadWA could not check the template. Nothing was sent. | Retry after a few seconds. |
MARKETING_SUPPRESSEDTemplate send |
409 | The recipient does not receive marketing messages from your account (for example, they opted out). The template is a marketing template, or SanadWA does not know its category. | Do not send marketing to this person. Use a utility template if the message is not marketing. |
SUPPRESSION_CHECK_FAILEDTemplate send |
503 | SanadWA could not check the recipient’s marketing preference. Nothing was sent. | Retry after a few seconds. |
invalid_to_phoneText send |
400 | to_phone is not a valid phone number. |
Send the number in E.164 format, for example +12025550123. |
customer_service_window_closedText send |
409 | The recipient has not written to this number in the last 24 hours, so free-form text is not allowed. details has last_inbound_at, window_expires_at and template_required: true. |
Send a template instead. |
message_not_foundGET /messages/{id} |
404 | No sent message with this id in your account, or the id does not start with wamid.. |
Check the id. Use the message_id from the send answer. |
message_lookup_failedGET /messages/{id} |
500 | A problem on our side. | Retry after a few seconds. |
templates_lookup_failedGET /templates |
500 | A problem on our side. | Retry after a few seconds. |
phone_numbers_lookup_failedGET /phone-numbers |
500 | A problem on our side. | Retry after a few seconds. |
balance_lookup_failedGET /balance |
500 | The balance could not be read. The API never reports a balance it could not read. | Retry after a few seconds. |
tag_already_existsPOST /tags |
409 | A tag with this name already exists (names are compared without regard to case). | Use the existing tag. |
tag_not_foundTags, contacts, campaigns |
404 | A tag id or tag name does not exist in your account. For a name list, details.tags lists the missing names. |
Create the tag with POST /tags, or check the id or name. |
tags_lookup_failedGET /tags |
500 | A problem on our side. | Retry after a few seconds. |
tag_create_failedPOST /tags |
500 | A problem on our side. The tag may not have been created. | Check with GET /tags, then retry. |
tag_delete_failedDELETE /tags/{id} |
500 | A problem on our side. | Retry after a few seconds. |
contact_not_foundDELETE /contacts/{id} |
404 | No contact with this id in your account. | Check the id. |
contact_opted_outDELETE /contacts/{id} |
409 | The contact opted out of marketing messages. Deleting it would erase the opt-out, so it is refused. details.suppression_state gives the kind of opt-out. |
Keep the contact. |
import_file_missingPOST /contacts/import |
400 | No CSV file in the file field. |
Send the file as multipart/form-data in the file field. |
import_file_too_largePOST /contacts/import |
413 | The file is larger than 5 MB. Nothing was saved. | Split the file. |
import_file_not_csvPOST /contacts/import |
415 | The file is an Excel workbook. Nothing was saved. | Save it as CSV (UTF-8) and upload that. |
import_csv_invalidPOST /contacts/import |
400 | The file is not valid CSV. Nothing was saved. | Check its quotes and separators. |
import_csv_emptyPOST /contacts/import |
400 | The file has no contact rows. | Add rows under the header line. |
import_too_many_rowsPOST /contacts/import |
400 | More than 5,000 contact rows. Nothing was saved. details.max_rows is the limit. |
Split the file. |
contacts_lookup_failedGET /contacts |
500 | A problem on our side. | Retry after a few seconds. |
contacts_upsert_failedPOST /contacts |
500 | A problem on our side. | Retry. Writing the same contacts again is safe. |
contacts_import_failedPOST /contacts/import |
500 | A problem on our side. | Retry. Importing the same file again is safe. |
contact_delete_failedDELETE /contacts/{id} |
500 | A problem on our side. | Retry after a few seconds. |
idempotency_key_requiredPOST /campaigns |
400 | The Idempotency-Key header is missing. Nothing was created. |
Send a unique key per campaign. See Campaigns. |
template_variables_invalidPOST /campaigns |
400 | template_variables does not match the template’s body variables. |
Give one entry for each body variable, in the template’s order. |
header_media_requiredPOST /campaigns |
400 | The template has an image, video or document header. | Send header_media_url. |
header_media_too_largePOST /campaigns |
400 | The file at header_media_url is too large. |
Use a smaller file. |
tracked_destination_requiredPOST /campaigns |
400 | The template has a click-tracked button with no saved destination. | Send tracked_destination_url. |
tracked_destination_invalidPOST /campaigns |
400 | tracked_destination_url is not an allowed link. |
Use a full https link. |
tracked_destination_not_applicablePOST /campaigns |
400 | The template has no click-tracked button. | Remove tracked_destination_url. |
campaign_invalidPOST /campaigns |
400 | Another campaign check failed. message explains it. |
Fix the campaign settings. |
insufficient_creditsCampaign create and resume |
402 | Your credits do not cover every recipient (create) or every message still to send (resume). details has needed, remaining and shortfall. Nothing was created or resumed. |
Buy credits, then retry. |
subscription_lapsedCampaign create and resume |
402 | Your subscription ended and there are not enough credits. | Renew the subscription or buy credits, then retry. |
kyc_requiredPOST /campaigns |
403 | Your account is not verified yet. | Verify your account in the console under Settings → Verification & privacy. |
tracked_links_disabledPOST /campaigns |
403 | The template has click-tracked buttons, and click tracking is not enabled for your account. | Choose another template, or contact support. |
template_not_foundPOST /campaigns |
404 | No template with this name and language for this number’s WhatsApp Business Account. | Check with GET /templates?phone_number_id=. |
campaign_not_foundCampaigns |
404 | No campaign with this id in your account. | Use an id from POST /campaigns or GET /campaigns. |
template_not_approvedPOST /campaigns |
409 | The template is not approved. details.template_status gives its status. |
Wait for approval, or choose an approved template. |
campaign_not_activeCampaign pause |
409 | Only an active campaign can be paused. details.status gives its status. |
Check the status with GET /campaigns/{id}. |
campaign_not_pausedCampaign resume |
409 | Only a paused campaign can be resumed. details.status gives its status. |
Check the status with GET /campaigns/{id}. |
campaign_cannot_resumeCampaign resume |
409 | The campaign cannot continue. message says why. |
Check the campaign in the console. |
campaign_busyCampaign pause and resume |
409 | Another change to this campaign is in progress. | Retry after Retry-After seconds. |
audience_emptyPOST /campaigns |
422 | No contact has any of the tags. Nothing was created. | Add contacts to the tags, or choose other tags. |
audience_all_excludedPOST /campaigns |
422 | Every contact with the tags is excluded (opted out, paused after a delivery problem, or not a valid WhatsApp number). details.contacts_with_tags gives how many have the tags. |
Choose other tags. |
audience_too_largePOST /campaigns |
422 | The audience is larger than one campaign allows. | Split the audience into smaller campaigns. |
balance_check_failedCampaign create and resume |
503 | Your balance could not be checked. Nothing was created or changed. | Retry after Retry-After seconds (with the same Idempotency-Key for a create). |
campaign_create_timeoutPOST /campaigns |
503 | Creating the campaign took too long and was stopped. Nothing was created or sent. | Retry with the same Idempotency-Key after Retry-After seconds. |
campaign_change_timeoutCampaign pause and resume |
503 | The change took too long and was stopped. Nothing was changed. | Retry after Retry-After seconds. |
campaigns_lookup_failedGET /campaigns |
500 | A problem on our side. | Retry after a few seconds. |
campaign_lookup_failedGET /campaigns/{id} |
500 | A problem on our side. | Retry after a few seconds. |
campaign_create_failedPOST /campaigns |
500 | A problem on our side. The campaign may or may not have been created. | Retry with the same Idempotency-Key: it returns the campaign if it was created. |
campaign_pause_failedCampaign pause |
500 | A problem on our side. | Check the status with GET /campaigns/{id}, then retry. |
campaign_resume_failedCampaign resume |
500 | A problem on our side. | Check the status with GET /campaigns/{id}, then retry. |
Under each code is where it can happen. “All” means every endpoint. “Writes” means every POST
and DELETE endpoint. “Sends” means POST /messages/template and POST /messages/text.
“JSON bodies” means requests with a JSON body.
Errors inside a 200 answer
Section titled “Errors inside a 200 answer”POST /contacts and POST /contacts/import handle each contact on its own and answer 200 even
when some contacts were not saved. Each contact that was not saved has its own error code:
| Code | Meaning | What to do |
|---|---|---|
validation_error |
A field of this contact is missing or wrong; message names it. |
Fix the contact and send it again. |
duplicate_phone |
The same phone number appears earlier in the same request. | Send each phone once. |
tag_not_found |
A tag name on this contact is not in your tag list. | Create the tag with POST /tags, then send the contact again. |
contact_write_failed |
The contact could not be saved because of a temporary problem on our side. | Send it again. |
See Import contacts and tag them.
send_failed
Section titled “send_failed”send_failed means the send did not complete. The HTTP status tells you more:
- A
4xxstatus (except408and429) means WhatsApp refused the message. Nothing was sent. The HTTP status is the status that WhatsApp (Meta) returned. - A
5xx,408or429status means the result is not known: the message may or may not have been sent. Read When the result of a send is not known before you retry. - A
409status with the messageThis send is already being processed.,This send requires reconciliation before another attempt.orSend request changed.comes from idempotency, not from WhatsApp.
A send_failed body has these extra fields:
| Field | Meaning |
|---|---|
upstream_code |
The WhatsApp (Meta) error code as a string, or null when the error did not come from WhatsApp. |
meta_error |
Present when WhatsApp returned an error: title, message, code, subcode, type and fbtrace_id. |
recovery |
Only when WhatsApp refused because of a payment problem on your Meta account (upstream_code 131042). It has a link where you can fix the payment method. |
{ "success": false, "code": "send_failed", "message": "template name (order_update) does not exist in en", "upstream_code": "132001", "meta_error": { "title": "Template name does not exist in the translation", "message": "template name (order_update) does not exist in en", "code": 132001, "subcode": null, "type": "OAuthException", "fbtrace_id": "AbCdEfGhIjKlMnOp" }}For the meaning of WhatsApp error codes, see Meta’s
WhatsApp Cloud API error codes.
When you contact support, include fbtrace_id.