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 theX-Request-Idheader. 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:
| Header | Always present | Purpose |
|---|---|---|
X-Request-Id | Yes | req_<12-hex> (or whatever you sent). Log it. Quote it in support tickets. |
X-RateLimit-Limit-Minute | Yes | Your per-minute budget |
X-RateLimit-Remaining-Minute | Yes | Requests left in the current minute |
X-RateLimit-Limit-Day | Yes | Your per-day budget |
X-RateLimit-Remaining-Day | Yes | Requests left in the current day |
X-RateLimit-Reset | Yes | Unix epoch seconds when the current window resets |
Retry-After | Only on 429 | Seconds to wait before the next request |
See Rate Limits for the full backoff guidance.
HTTP status codes
| Status | When you see it |
|---|---|
200 | Success |
201 | Resource created (e.g. POST /contacts, POST /webhooks) |
400 | Validation error — missing/invalid field, malformed body or query param (code: VALIDATION_ERROR, with a details array) |
401 | Missing, malformed, expired, or revoked API key (code: AUTHENTICATION_REQUIRED) |
403 | Authenticated, 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) |
404 | The resource ID does not exist (or belongs to another tenant) (code: NOT_FOUND) |
409 | Conflict — usually a unique-constraint violation (code: CONFLICT) |
429 | Rate-limited — back off until the time in Retry-After (code: RATE_LIMIT_EXCEEDED) |
500 | Lynkist encountered an unexpected error — safe to retry with backoff |
502, 503, 504 | Transient upstream issue — retry with backoff |
Common error messages by category
Authentication (401, code: AUTHENTICATION_REQUIRED)
error.message | Meaning |
|---|---|
Not authenticated | No Authorization header (or wrong scheme) |
Invalid API key format | Token does not match lk_sk_{live|test}_{40-hex} |
Invalid or revoked API key | Token does not match any active key |
API key has expired | Key passed its expires_at |
Tenant not found | Key resolves to a tenant that no longer exists |
Authorisation (403)
error.code / error.message | Meaning |
|---|---|
FORBIDDEN + required_permissions / current_permissions | Key is missing one or more required scopes |
FORBIDDEN — Request IP not in allowlist | Caller's IP is outside the key's IP allowlist |
UPGRADE_REQUIRED + feature_key, upgrade_required: true | Your 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.message | Meaning |
|---|---|
Contact not found | The contact ID does not exist or belongs to another tenant |
No approved template named '…' | No approved template with that name (on send) |
Media not found | The media ID does not exist |
No active WABA account for this tenant | The 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 — for403— the extra fields inerror. - Log
X-Request-Idon 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.
5xxand429are retryable;4xx(except409against the sameIdempotency-Key) are bugs in the request and will not pass on retry. - Pair retries with
Idempotency-Key. A retriedPOSTwithout an idempotency key risks creating duplicate resources. See API Reference. - Treat
403 not in allowlistas fatal. A network move is the only fix; retrying from the same IP will fail forever.