Skip to content
Contents
Reference

Errors and limits

One envelope for every failure, one stable code per reason, and a request id on every response. This page is the whole failure surface: the codes, the statuses, cursor pagination and rate limiting.

The error envelope

Every error, from every endpoint, has the same shape. There is no second format and no bare string response.

HTTP
HTTP/1.1 422 Unprocessable Entity
content-type: application/json; charset=utf-8
x-request-id: req_...

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request payload failed validation.",
    "request_id": "req_...",
    "details": {
      "issues": [
        { "path": "to", "message": "must be an E.164 number, e.g. +9665XXXXXXXX" }
      ]
    }
  }
}
FieldDescription
error.codeA stable machine-readable identifier. This is what your code branches on.
error.messageEnglish text for a human. Useful in a log; never match on it.
error.request_idAlso the X-Request-Id response header. It identifies this exact request in the platform's logs.
error.detailsPresent for some codes only. Its shape depends on the code — a validation error carries issues; an entitlement error carries the limit and the current count.

Branch on code, never on message

The code is part of the API contract and will not change meaning without a version bump. The message is human text and can be reworded at any time. Matching on it is the fastest way to build an integration that breaks on a copy edit.

Validation errors

A malformed request is a 422 with a machine-readable list of what was wrong.

  • Validation failures are 422, not 400. The only 400 in the whole API is a missing workspace context.
  • docs.pages.errors.validation.rules.issues
  • The value you sent is never echoed back, so an error is safe to log even when the request carried something sensitive.
  • Most write bodies are strict: an unrecognised field is an error rather than an ignored key. A misspelled field name fails loudly.

Error codes

The complete public catalogue: 72 codes, grouped as the platform groups them. The message column is the API's own English string, reproduced exactly — it is response data rather than documentation prose, which is why it is not translated.

Codes may be added to this list. An existing code never changes meaning or disappears without an API version bump.

Transport and authentication

CodeHTTPDefault message returned by the API
UNAUTHORIZED401Authentication is required or the provided credentials are invalid.
FORBIDDEN403You do not have permission to perform this action.
NOT_FOUND404The requested resource was not found.
METHOD_NOT_ALLOWED405That HTTP method is not allowed on this resource.
VALIDATION_ERROR422The request payload failed validation.
CONFLICT409The request conflicts with the current state of the resource.
PAYLOAD_TOO_LARGE413The request payload is too large.
UNSUPPORTED_MEDIA_TYPE415The request content type is not supported.
RATE_LIMITED429Too many requests. Please retry later.
IDEMPOTENCY_CONFLICT409This idempotency key was already used with a different request payload.
INTERNAL_ERROR500An unexpected error occurred while processing the request.
SERVICE_UNAVAILABLE503The service is temporarily unavailable. Please retry later.

Accounts and sign-in

CodeHTTPDefault message returned by the API
INVALID_CREDENTIALS401The email address or password is incorrect.
EMAIL_ALREADY_IN_USE409An account already exists for that email address.
EMAIL_NOT_VERIFIED403Verify your email address before continuing.
ACCOUNT_SUSPENDED403This account is suspended.
TOKEN_INVALID401That link or token is invalid or has already been used.
TOKEN_EXPIRED401That link or token has expired. Request a new one.

Workspaces, members and teams

CodeHTTPDefault message returned by the API
WORKSPACE_NOT_FOUND404The requested workspace was not found.
WORKSPACE_SUSPENDED403This workspace is suspended.
WORKSPACE_CONTEXT_REQUIRED400A workspace must be specified for this request.
MEMBER_NOT_FOUND404The requested workspace member was not found.
MEMBER_ALREADY_EXISTS409That person is already a member of this workspace.
OWNER_REQUIRED403Only the workspace owner can perform this action.
INVITATION_NOT_FOUND404The requested invitation was not found.
TEAM_NOT_FOUND404The requested team was not found.

API keys and plans

CodeHTTPDefault message returned by the API
API_KEY_NOT_FOUND404The requested API key was not found.
API_KEY_REVOKED401This API key has been revoked.
PLAN_NOT_FOUND404The requested plan was not found.
ENTITLEMENT_LIMIT_REACHED409The current plan limit for this resource has been reached.

Channels and providers

CodeHTTPDefault message returned by the API
CHANNEL_NOT_FOUND404The requested channel was not found.
CHANNEL_NOT_CONNECTED409The selected channel is not connected.
CHANNEL_CAPABILITY_UNAVAILABLE409The selected channel does not support this capability.
PROVIDER_RATE_LIMITED429The upstream messaging provider is rate limiting this channel.
PROVIDER_REJECTED502The upstream messaging provider rejected this request.

Sandbox

CodeHTTPDefault message returned by the API
SANDBOX_RECIPIENT_NOT_VERIFIED403The recipient is not verified for this sandbox allocation.
SANDBOX_TEST_KEY_REQUIRED403A test-mode API key is required for sandbox operations.
SANDBOX_SESSION_NOT_FOUND404The requested sandbox session was not found.
SANDBOX_SESSION_EXPIRED409This sandbox session has expired. Request a new one.
SANDBOX_NO_NUMBER_AVAILABLE503No shared test number is available right now. Please retry shortly.
SANDBOX_VERIFICATION_PENDING409Send the verification code from the recipient number to the assigned test number to complete verification.
SANDBOX_VERIFICATION_INVALID422That verification code is incorrect.
SANDBOX_VERIFICATION_EXPIRED409That verification code has expired. Request a new one.

Contacts, groups and imports

CodeHTTPDefault message returned by the API
CONTACT_NOT_FOUND404The requested contact was not found.
CONTACT_ALREADY_EXISTS409A contact with that phone number already exists in this workspace.
CONTACT_GROUP_NOT_FOUND404The requested contact group was not found.
TAG_NOT_FOUND404The requested tag was not found.
IMPORT_NOT_FOUND404The requested import was not found.
IMPORT_FILE_TOO_LARGE413The uploaded file is larger than the import limit.
IMPORT_FORMAT_UNSUPPORTED415That file format is not supported for contact import.

Conversations and assignment

CodeHTTPDefault message returned by the API
CONVERSATION_NOT_FOUND404The requested conversation was not found.
CONVERSATION_ACCESS_DENIED403You do not have access to this conversation.
ASSIGNEE_NOT_FOUND404The requested assignee was not found in this workspace.
CONVERSATION_CONFLICT409This conversation was changed by someone else. Reload and try again.
NOTE_NOT_FOUND404The requested note was not found.

Templates

CodeHTTPDefault message returned by the API
TEMPLATE_NOT_FOUND404The requested template was not found.
TEMPLATE_NOT_APPROVED409The requested template is not approved for sending.
TEMPLATE_ALREADY_EXISTS409A template with that name and language already exists.
TEMPLATE_VARIABLE_MISSING422One or more template variables were not supplied.
TEMPLATE_NOT_SENDABLE409This template is not available for sending.
META_SERVICE_WINDOW_REQUIRED_TEMPLATE409The customer service window is closed. Send an approved template instead.

QR

CodeHTTPDefault message returned by the API
QR_SAFETY_LIMIT_REACHED429The channel safety limit for this period has been reached.

Webhooks

CodeHTTPDefault message returned by the API
WEBHOOK_ENDPOINT_NOT_FOUND404The requested webhook endpoint was not found.
WEBHOOK_DELIVERY_NOT_FOUND404The requested webhook delivery was not found.
WEBHOOK_ENDPOINT_DISABLED409This webhook endpoint was disabled after repeated delivery failures. Update it and set it back to active.
WEBHOOK_URL_NOT_ALLOWED422That webhook URL is not accepted by this environment.

Campaigns and automations

Reserved codes

These codes are defined and stable, but no endpoint raises them: campaigns and automations are not built. They are listed so a client that already handles them is not surprised later, not because you will see them today.

CodeHTTPDefault message returned by the API
CAMPAIGN_NOT_FOUND404The requested campaign was not found.
CAMPAIGN_PAUSED409The campaign is paused.
CAMPAIGN_INVALID_STATE409That action is not available while the campaign is in its current state.
CAMPAIGN_AUDIENCE_EMPTY422The selected audience contains no contacts that may receive a marketing message.
CAMPAIGN_CHANNEL_NOT_ELIGIBLE409The selected channel cannot be used to run a campaign.
AUTOMATION_LIMIT_REACHED409The automation execution limit has been reached.

HTTP statuses

The status tells you the class of problem; the code tells you which one.

HTTPDescription
400A workspace was not specified. This is the only 400 in the API.
401No credential, or one that is not valid.
403A valid credential that is not permitted here, or a suspended account or workspace.
404The resource does not exist, or belongs to another workspace — the two are deliberately indistinguishable.
409The request conflicts with the current state: a channel that is not connected, a plan limit, an idempotency conflict.
413The payload is larger than the limit.
415The content type or file format is not supported.
422The payload failed validation. Check details.issues.
429Rate limited. Read Retry-After.
500An unexpected failure on the platform. Safe to retry.
502The upstream messaging provider rejected the request.
503A dependency is temporarily unavailable. Safe to retry.

Validation is 422, not 400

If your client treats 400 as the validation status, it will miss every validation error this API produces. The one 400 is WORKSPACE_CONTEXT_REQUIRED.

Pagination

List endpoints are cursor-paginated. Read page.next_cursor and pass it back as after; stop when has_more is false.

curl
curl "https://whats.azzamkh.sa/api/v1/messages?limit=50" \
  -H "Authorization: Bearer $WA_API_KEY"

# 200 OK
# {
#   "data": [ { "id": "msg_...", ... } ],
#   "page": { "next_cursor": "bXNnXzAxSj...", "has_more": true }
# }

# Follow the cursor. Send it back verbatim; it is opaque.
curl "https://whats.azzamkh.sa/api/v1/messages?limit=50&after=bXNnXzAxSj..." \
  -H "Authorization: Bearer $WA_API_KEY"
ParameterDefaultDescription
limit50Clamped to 1–100.
afterThe previous page's next_cursor.
  • An out-of-range or unparseable limit is clamped, never rejected. Asking for 5000 rows returns 100.
  • The cursor is opaque. Send it back verbatim and do not decode it — a malformed one is ignored and you silently get the first page again.
  • Most lists are newest-first, but templates are oldest-first. Do not assume a direction; follow the cursor.
  • next_cursor is null on the last page. Loop on has_more, not on an empty data array.

Rate limiting

Rate-limited routes answer 429 with the standard headers and a retry hint in the body.

HTTP
HTTP/1.1 429 Too Many Requests
ratelimit-limit: 600
ratelimit-remaining: 0
ratelimit-reset: 37
retry-after: 37

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please retry later.",
    "request_id": "req_...",
    "details": { "bucket": "messages.send", "retry_after_s": 37 }
  }
}
HeaderDescription
RateLimit-LimitThe ceiling for this bucket and window.
RateLimit-RemainingHow many requests are left in the current window. Zero on a denial.
RateLimit-ResetSeconds until the window resets.
Retry-AfterSeconds to wait. Present only on a denial. Honour it.
  • Every value is in seconds, not a timestamp.
  • The headers are absent when rate limiting is switched off for the environment. Treat their absence as no information, not as an unlimited quota.
  • The limiter fails open: if its backing store is unreachable, requests are allowed. Never rely on it as a correctness mechanism.
  • PROVIDER_RATE_LIMITED is a different thing: the upstream WhatsApp provider is throttling the channel, and no header will tell you when it stops.

What to retry

Retry with exponential backoff and jitter, and always send an Idempotency-Key on a write so a retry cannot duplicate anything.

CodeWhat to do
RATE_LIMITEDWait for Retry-After, then retry. Do not retry sooner.
PROVIDER_RATE_LIMITEDBack off harder — the upstream provider is throttling, and there is no header telling you when it stops.
SERVICE_UNAVAILABLERetry with backoff. A dependency is temporarily down.
INTERNAL_ERRORRetry with backoff. If it persists, quote the request_id.
PROVIDER_REJECTEDDo not retry blindly. The provider refused the message; inspect it first.
VALIDATION_ERRORNever retry. Fix the request.
IDEMPOTENCY_CONFLICTNever retry as-is. Either the body or the key is wrong.