Authentication
API keys, scopes, IP allowlists, and rotation for the Lynkist Public API.
The Lynkist Public API uses bearer-token authentication. Every request must include an
Authorization: Bearer <api_key> header.
API key format
API keys always have four underscore-separated parts:
lk_sk_<env>_<40-character-hex-secret>
│ │
│ └── 40 random hex characters
└── live | test| Type | Prefix | Use |
|---|---|---|
| Live | lk_sk_live_… | Production traffic |
| Test | lk_sk_test_… | A label for keys you use in dev/CI |
There is no separate sandbox environment — live and test keys hit the same API and the
same data. The test prefix is purely an organisational label so you can tell keys apart and
revoke them independently; it does not route to an isolated environment.
Treat API keys like passwords. Do not commit them to source control, ship them in client-side bundles, log them, or paste them into chat. Store them in environment variables or a secrets manager.
Creating a key
API key creation is dashboard-only (JWT-authenticated). The Public API itself cannot mint keys.
- In the dashboard, open Settings → API keys. The mount is
/{tenant}/api/v1/settings/api-keys. - Click Create new key.
- Pick a name (visible to your team), the environment (
liveortest) and one or more scopes (see Scopes below). - Copy the secret immediately — it is shown once and never again.
The response contains the metadata plus the plain-text secret:
{
"api_key": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Server-side messaging",
"key_prefix": "lk_sk_live_a",
"scopes": ["messages:send", "templates:read"],
"environment": "live",
"is_active": true,
"created_at": "2026-05-31T08:00:00Z",
"last_used_at": null
},
"secret": "lk_sk_live_a1b2c3d4…"
}Only the key_prefix (the first 12 characters) is stored on Lynkist's side after creation;
the secret is irrecoverable. If you lose it, rotate the key.
Making authenticated requests
Pass the secret in the Authorization header.
curl https://lynkist.io/api/v1/public/contacts \
-H "Authorization: Bearer lk_sk_live_..."If the key is missing, invalid, expired, or revoked, the API returns 401 Unauthorized. If the
key is valid but lacks the scope for the route, you get 403 Forbidden — see
Errors for the response shape.
Scopes
A key carries a list of resource:action strings. Lynkist enforces the relevant scope on every
endpoint; a request missing the required scope is rejected with 403. The wildcard scope *
satisfies every check.
Full catalogue (12 scopes):
| Scope | Allows |
|---|---|
contacts:read | List, search, get contacts |
contacts:write | Create, update, bulk-create, archive contacts |
templates:read | List, get approved templates |
templates:write | Create and submit templates for approval |
messages:send | Send template messages |
media:read | List, get media |
media:write | Upload media, create resumable upload sessions, delete media |
webhooks:read | List endpoints, list deliveries, view metrics |
webhooks:write | Create / update / delete endpoints, rotate secret, retry, send tests |
campaigns:read | List, get, preview, metrics, errors |
campaigns:write | Create, set audience, render preview, refresh metrics |
campaigns:send | Send, schedule, pause, resume, stop |
Presets
When creating a key in the dashboard you can pick one of four presets instead of hand-selecting scopes:
| Preset | Scopes |
|---|---|
read_only | contacts:read, templates:read, media:read, webhooks:read, campaigns:read |
messaging | contacts:read, templates:read, media:write, messages:send |
campaigns | contacts:read, templates:read, media:read, campaigns:read, campaigns:write, campaigns:send |
full_access | * (wildcard — every scope, current and future) |
Scope-denial response
When a key is missing a required scope, the response body explicitly lists what was required versus what was held — useful for surfacing "you need to ask your admin for X" prompts inside integrations:
{
"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": "…" }
}IP allowlists & expiry
A key can be locked to a set of source IPs or CIDR ranges, and/or given an expiry. When an
allowlist is set, requests from any other address return 403 (Request IP not in allowlist),
even if the key and scopes are otherwise valid; an expired key returns 401 API key has expired.
Allowlist entries can be either plain IPs (203.0.113.7) or CIDR (203.0.113.0/24).The key-creation request currently accepts only name, scopes, and environment. IP
allowlists and expiry are enforced when present, but are not yet settable through the
create API — contact Lynkist if you need them provisioned on a key. Self-service configuration
is on the roadmap; track Changelog.
Rotating a key
- From the dashboard, click Rotate on the existing key. Lynkist mints a new secret with the same scopes and environment and returns it once.
- Deploy the new secret to every service that uses it.
- The old key is revoked immediately as part of the rotate operation — there is no grace period.
The rotate endpoint is POST /{tenant}/api/v1/settings/api-keys/{key_id}/rotate. Plain revoke
(without minting a replacement) is POST .../revoke.
Best practices
- One key per service. Issue a distinct key for each downstream service so you can revoke one without taking another offline.
- Minimum scopes. A campaign-only service does not need
messages:send. Start narrow. - IP-lock long-lived keys. Anything running on a known set of servers should be allowlisted.
- Watch
last_used_at. The dashboard shows it on every key. Revoke anything idle for 90+ days. - Pair with
Idempotency-Key. See API Reference for safe-retry semantics.
OAuth-style delegated access (for building third-party integrations against multiple tenants) is not available yet. Today, all programmatic access is via per-tenant bearer keys.
Dashboard login lockout
The API-key auth above is separate from dashboard sign-in (the email + password login that issues the JWT used to manage keys, templates, and campaigns). To blunt brute-force attempts, dashboard login enforces an account lockout:
- After 3 consecutive failed password attempts, the account is locked.
- While locked,
POST /api/v1/login/access-tokenreturns423 Locked— even if the correct password is supplied — until the account is explicitly unlocked. (This is distinct from the IP-based429throttle, which is temporary and clears on its own.) - A locked account is re-opened in one of two ways, both of which reset the failed-attempt
counter:
- Phone OTP — request a one-time code to the account's registered phone
(
POST /api/v1/login/unlock/phone/request), then verify it (POST /api/v1/login/unlock/phone/verify). - Password reset — completing the normal password-recovery flow also lifts the lock.
- Phone OTP — request a one-time code to the account's registered phone
(
Lockout applies uniformly to every account, superusers included. There is no time-based auto-unlock — a locked account stays locked until a successful phone-OTP verification or password reset. End-user steps live in the Help Center.