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 has a single top-level detail field. The contents are usually a string,
but a few endpoints return a typed object when the structured form is more useful to clients
(e.g. scope-denial). Both shapes are supported as the contract:
String form (the default)
{ "detail": "Invalid or revoked API key" }Object form
Returned by endpoints that have structured information for the caller. The current example is a
missing-scope 403, which lists exactly what was required vs. what the key holds — useful for
UI prompts like "ask an admin for campaigns:send":
{
"detail": {
"message": "Missing required scope(s): campaigns:send",
"required_permissions": ["campaigns:send"],
"current_permissions": ["campaigns:read", "campaigns:write"]
}
}Clients should accept both forms. Treat typeof detail === 'string' ? detail : detail.message
as the human-readable message.
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 | Malformed request body, missing required field, invalid format |
401 | Missing, malformed, expired, or revoked API key |
403 | Authenticated, but the key lacks the required scope or the request IP is not in the allowlist |
404 | The resource ID does not exist (or belongs to another tenant) |
409 | Conflict — usually a unique-constraint violation |
422 | Validation error — the body is well-formed but semantically invalid (Pydantic validation) |
429 | Rate-limited — back off until the time in Retry-After |
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)
detail | 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)
detail | Meaning |
|---|---|
Object form with required_permissions / current_permissions | Key is missing one or more required scopes |
Request IP not in allowlist | Caller's IP is outside the key's IP allowlist |
API access is not enabled for your plan | Tenant is on a plan that disables API access (e.g. free) |
Tenant context missing | Internal — should not occur in normal request paths |
Validation (400 / 422)
Pydantic-driven validation failures use FastAPI's default 422 body, which lists every offending field:
{
"detail": [
{
"type": "missing",
"loc": ["body", "to"],
"msg": "Field required",
"input": { "template_name": "hello_world" }
}
]
}Validate request bodies on your side against the schemas in the per-resource API Reference.
Resource (404 / 409)
detail | Meaning |
|---|---|
Contact not found | The contact ID does not exist or belongs to another tenant |
Template not found | The template ID does not exist or is not approved |
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)
{ "detail": "Rate limit exceeded. Please slow down." }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, plus — for
403— the structure of thedetailobject. - 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.