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 (e.g. AUTHENTICATION_REQUIRED, FORBIDDEN, NOT_FOUND, VALIDATION_ERROR, RATE_LIMIT_EXCEEDED, UPGRADE_REQUIRED, CONFLICT, INTERNAL_ERROR). Branch on this, not on the message.
  • error.message — a human-readable string. Written for people; may change.
  • meta.request_id — the same value as the X-Request-Id header. Log it; quote it in tickets.

Some errors add extra fields alongside code/message inside error. For example a missing-scope 403 lists what was required vs. held:

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

Other examples: a plan-gate 403 adds upgrade_required: true and feature_key; a validation error adds a details array (see below).

Success responses are not wrapped in this envelope — they return the resource (or a { "data": [...], "pagination": {...} } list) directly. The success/error/meta envelope is error-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
Retry-AfterOnly on 429Seconds to wait before the next request

See Rate Limits for the full backoff guidance.

HTTP status codes

StatusWhen you see it
200Success
201Resource created (e.g. POST /contacts, POST /webhooks)
400Validation error — missing/invalid field, malformed body or query param (code: VALIDATION_ERROR, with a details array)
401Missing, malformed, expired, or revoked API key (code: AUTHENTICATION_REQUIRED)
403Authenticated, but the key lacks the required scope, the request IP is not in the allowlist, or your plan doesn't include the feature (code: FORBIDDEN / UPGRADE_REQUIRED)
404The resource ID does not exist (or belongs to another tenant) (code: NOT_FOUND)
409Conflict — usually a unique-constraint violation (code: CONFLICT)
429Rate-limited — back off until the time in Retry-After (code: RATE_LIMIT_EXCEEDED)
500Lynkist encountered an unexpected error — safe to retry with backoff
502, 503, 504Transient upstream issue — retry with backoff

Common error messages by category

Authentication (401, code: AUTHENTICATION_REQUIRED)

error.messageMeaning
Not authenticatedNo Authorization header (or wrong scheme)
Invalid API key formatToken does not match lk_sk_{live|test}_{40-hex}
Invalid or revoked API keyToken does not match any active key
API key has expiredKey passed its expires_at
Tenant not foundKey resolves to a tenant that no longer exists

Authorisation (403)

error.code / error.messageMeaning
FORBIDDEN + required_permissions / current_permissionsKey is missing one or more required scopes
FORBIDDENRequest IP not in allowlistCaller's IP is outside the key's IP allowlist
UPGRADE_REQUIRED + feature_key, upgrade_required: trueYour plan doesn't include this feature (e.g. API access, webhooks)

Validation (400, code: VALIDATION_ERROR)

Validation failures return 400 (not 422). The offending fields are listed in error.details:

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid fields.",
    "details": [
      {
        "type": "missing",
        "loc": ["body", "to"],
        "msg": "Field required",
        "input": { "template_name": "hello_world" }
      }
    ]
  },
  "meta": { "request_id": "req_…", "timestamp": "…" }
}

Validate request bodies on your side against the schemas in the per-resource API Reference.

Resource (404 / 409)

error.messageMeaning
Contact not foundThe contact ID does not exist or belongs to another tenant
No approved template named '…'No approved template with that name (on send)
Media not foundThe media ID does not exist
No active WABA account for this tenantThe tenant has no connected, active WhatsApp Business Account

Rate limiting (429, code: RATE_LIMIT_EXCEEDED)

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

Sleep until the timestamp in X-RateLimit-Reset (or use the easier Retry-After seconds) and try again. See Rate Limits for the per-plan budgets.

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. 5xx and 429 are retryable; 4xx (except 409 against the same Idempotency-Key) are bugs in the request and will not pass on retry.
  • Pair retries with Idempotency-Key. A retried POST without an idempotency key risks creating duplicate resources. See API Reference.
  • Treat 403 not in allowlist as fatal. A network move is the only fix; retrying from the same IP will fail forever.
Errors — Lynkist Developers | Lynkist