Lynkist Developers

Errors

Status codes, response envelopes, and recovery strategies for the Lynkist Public API.

The Lynkist Public API uses standard HTTP status codes and a small, predictable JSON body. This page describes the envelopes you actually receive today, the HTTP codes you should plan for, and the headers that help you correlate, retry, and back off.

Error envelope

Every error response uses the same JSON envelope:

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Contact not found"
  },
  "meta": {
    "request_id": "req_04673bffe2ed",
    "timestamp": "2026-06-28T05:22:51.953Z"
  }
}
  • error.code — a stable, machine-readable code from the list below. Branch on this, not on the message. A code and its HTTP status do not change inside a major version (Versioning).
  • error.message — a sentence for a person. It can change.
  • meta.request_id — the same value as the X-Request-Id header. Log it; quote it in tickets.

Some codes add extra fields beside code and message. A missing scope lists what was required and what the key has:

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "Missing required scope(s): campaigns:send",
    "required_scopes": ["campaigns:send"],
    "granted_scopes": ["campaigns:read", "campaigns:write"]
  },
  "meta": { "request_id": "req_…", "timestamp": "…" }
}

Other examples: UPGRADE_REQUIRED adds upgrade_required: true and feature_key; RATE_LIMIT_EXCEEDED adds window and retry_after; a send error adds retryable; VALIDATION_ERROR adds a details list (see below).

Success responses are not in this envelope — they return the resource (or a { "data": [...], "pagination": {...} } list) directly. The success/error/meta envelope is for errors only.

Standard response headers

Every response — success or error — carries:

HeaderAlways presentPurpose
X-Request-IdYesreq_<12-hex> (or whatever you sent). Log it. Quote it in support tickets.
X-RateLimit-Limit-MinuteYesYour per-minute budget
X-RateLimit-Remaining-MinuteYesRequests left in the current minute
X-RateLimit-Limit-DayYesYour per-day budget
X-RateLimit-Remaining-DayYesRequests left in the current day
X-RateLimit-ResetYesUnix epoch seconds when the current window resets
Lynkist-VersionYesThe exact API version that answered, e.g. 1.0
Retry-AfterOn 429, and on 503 when knownSeconds to wait before the next request

See Rate Limits for the full backoff guidance.

HTTP status codes

StatusMeaningRetry?
200, 201Success—
400The request is not valid: a missing or wrong field, a bad cursor, a rejected sendNo. Fix the request.
401The API key is missing, wrong, expired or revokedNo
402Not enough wallet credit to sendAfter a top-up
403The key lacks a scope, the IP is not allowed, or the plan does not include the featureNo
404The resource does not exist (or belongs to another workspace)No
409The request conflicts with the current state (a duplicate contact, a paused template)After the state changes
429Rate limit. Wait for Retry-AfterYes, after the wait
500An unexpected error on our sideYes, with backoff
502, 503, 504A failure on our side or at WhatsAppYes, with backoff. But see SEND_OUTCOME_UNKNOWN.

A failure on our side is always 5xx, never 4xx. So the rule for a client is simple: retry 5xx and 429 with exponential backoff, and never retry another 4xx unchanged.

Error codes

CodeStatusDefault message
EMPTY_AUDIENCE400The audience is empty.
INVALID_CAMPAIGN_STATE400The campaign's status does not allow this.
INVALID_CURSOR400The pagination cursor is not valid. Start from the first page.
INVALID_REQUEST400The request cannot be processed as it is.
INVALID_SCHEDULE_TIME400The schedule time is not valid.
INVALID_TEMPLATE_PARAMETERS400The template or its parameters are not valid.
INVALID_TEMPLATE_STATE400The template's status does not allow this.
INVALID_VARIABLE_MAPPING400The variable mapping is not valid.
INVALID_WEBHOOK_URL400The webhook URL is not valid or not allowed.
MEDIA_REJECTED400The attached media could not be sent.
MESSAGE_SEND_FAILED400WhatsApp rejected the message.
NO_CAMPAIGN_RECIPIENTS400The campaign has no recipients.
RECIPIENT_UNDELIVERABLE400This number cannot receive the message.
TEMPLATE_LANGUAGE_ALREADY_EXISTS400A template with this name and language already exists.
TEMPLATE_LOCKED400The template is in review and cannot be changed now.
TEMPLATE_NOT_APPROVED400The template is not approved.
TEMPLATE_REJECTED400WhatsApp rejected the template or its variables.
VALIDATION_ERROR400The request contains invalid fields.
WEBHOOK_DELIVERY_ALREADY_SUCCEEDED400This delivery succeeded and cannot be sent again.
API_KEY_EXPIRED401The API key has expired.
AUTHENTICATION_REQUIRED401Access token is missing, expired, or invalid.
INVALID_API_KEY401The API key is not valid or was revoked.
INSUFFICIENT_WALLET_BALANCE402Insufficient wallet credit. Top up your wallet to send template messages.
FORBIDDEN403You do not have permission to perform this action.
INSUFFICIENT_SCOPE403The API key does not have the required scope.
IP_NOT_ALLOWED403This IP address is not in the API key's allowlist.
SENDING_NOT_ALLOWED403This workspace cannot send WhatsApp messages.
UPGRADE_REQUIRED403This feature is not included in your plan.
WORKSPACE_CLOSED403This workspace is closed or being deleted.
CAMPAIGN_NOT_FOUND404Campaign not found.
CONTACT_NOT_FOUND404Contact not found.
GROUP_NOT_FOUND404Contact group not found.
MEDIA_NOT_FOUND404Media not found.
NOT_FOUND404Resource not found
SOURCE_NOT_FOUND404Contact source not found.
TEMPLATE_NOT_FOUND404Template not found.
WEBHOOK_DELIVERY_NOT_FOUND404Webhook delivery not found.
WEBHOOK_NOT_FOUND404Webhook endpoint not found.
WHATSAPP_NOT_CONNECTED404No active WhatsApp Business account is connected to this workspace.
METHOD_NOT_ALLOWED405This method is not allowed on this endpoint.
ACCOUNT_RESTRICTED409This WhatsApp Business account is restricted.
CAMPAIGN_ALREADY_EXISTS409A campaign with this name already exists.
CONFLICT409Conflict
CONTACT_ALREADY_EXISTS409A contact with this phone or email already exists.
CUSTOMER_SERVICE_WINDOW_CLOSED409The 24-hour window is closed; only a template message can be sent.
DISPLAY_NAME_NOT_APPROVED409This WhatsApp number cannot send yet: its display name is not approved.
NUMBER_NOT_REGISTERED409This WhatsApp number is not fully registered.
TEMPLATE_UNAVAILABLE409This template is paused or disabled by Meta.
WHATSAPP_CONNECTION_ERROR409The WhatsApp connection for this number must be re-authorised.
WHATSAPP_PAYMENT_REQUIRED409Meta rejected the send because of a billing problem on the account.
PAYLOAD_TOO_LARGE413The request body is too large.
MEDIA_TYPE_NOT_SUPPORTED415This media type is not supported.
UNPROCESSABLE_REQUEST422The request is well-formed but cannot be applied.
PLAN_LIMIT_REACHED429You have reached your plan limit for this feature.
RATE_LIMIT_EXCEEDED429Too many requests. Please slow down.
SEND_RATE_LIMITED429WhatsApp is limiting sends from this number.
TEMPLATE_EDIT_QUOTA_EXCEEDED429Meta's edit limit for this template is reached.
INTERNAL_ERROR500An unexpected error occurred. Please try again later.
UPSTREAM_ERROR502A service we depend on returned an error. Please try again.
SERVICE_UNAVAILABLE503The service is temporarily unavailable. Please try again shortly.
WHATSAPP_UNAVAILABLE503WhatsApp sending is temporarily unavailable. Try again shortly.
SEND_OUTCOME_UNKNOWN504WhatsApp did not confirm the message in time; it may still be delivered.
UPSTREAM_TIMEOUT504A service we depend on did not answer in time. Please try again.

An error with no specific code has the code of its status: INVALID_REQUEST (400), NOT_FOUND (404), CONFLICT (409), UPSTREAM_ERROR (502), and so on. UPGRADE_REQUIRED is always 403 (your plan does not include the feature); a plan limit is 429 PLAN_LIMIT_REACHED.

A request with no Authorization header answers 401 AUTHENTICATION_REQUIRED with the message Not authenticated.

Send errors

POST /messages/template can answer with one of the send codes above (RECIPIENT_UNDELIVERABLE, TEMPLATE_REJECTED, SEND_RATE_LIMITED, WHATSAPP_UNAVAILABLE, …). Each has retryable:

{
  "success": false,
  "error": {
    "code": "WHATSAPP_UNAVAILABLE",
    "message": "WhatsApp sending is temporarily unavailable. Wait a moment, then send the message again.",
    "retryable": true
  },
  "meta": { "request_id": "req_…", "timestamp": "…" }
}

SEND_OUTCOME_UNKNOWN (504) means WhatsApp did not confirm the message in time. The message can still arrive. retryable is false: do not send it again automatically, or the customer can get it twice. Check the message.* webhook events first.

Validation errors (400 VALIDATION_ERROR)

The rejected fields are in error.details. Each entry names the field, a machine-readable code and a message. The values that you sent are never returned.

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid fields.",
    "details": [
      { "field": "to", "code": "missing", "message": "Field required" }
    ]
  },
  "meta": { "request_id": "req_…", "timestamp": "…" }
}

A nested field has a dotted path, for example contacts.0.phone.

For now each entry also has loc, msg and type, with the same information in an older form. They will be removed in a later major version; read field, code and message.

Rate limiting (429 RATE_LIMIT_EXCEEDED)

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Please slow down.",
    "window": "minute",
    "retry_after": 17
  },
  "meta": { "request_id": "req_…", "timestamp": "…" }
}

Wait the number of seconds in the Retry-After header, then try again. See Rate Limits.

Handling errors well

  • Don't pattern-match on the human message. Messages are written for people and may change. Match on the HTTP status and error.code, plus — for 403 — the extra fields in error.
  • Log X-Request-Id on every failure. It is the single fastest signal we can correlate against on our side. Include it in support tickets and bug reports.
  • Retry only what is safe. Retry 5xx and 429 with exponential backoff (for example 1 s, 2 s, 4 s, … with some random jitter, and a maximum number of attempts), and obey Retry-After. Another 4xx does not pass on a retry. A retry loop with no backoff makes an outage longer.
  • Pair retries with Idempotency-Key. A retried POST without an idempotency key risks creating duplicate resources. See API Reference.
  • Treat IP_NOT_ALLOWED as fatal. A network move is the only fix; retrying from the same IP will fail forever.
Errors | Lynkist Developers