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 |
| Sandbox | lk_sk_test_… | Local development, CI, integration tests |
Live and sandbox keys hit the same base URL — what changes is only the prefix on the key itself.
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_",
"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://api.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:
{
"detail": {
"message": "Missing required scope(s): campaigns:send",
"required_permissions": ["campaigns:send"],
"current_permissions": ["campaigns:read", "campaigns:write"]
}
}IP allowlists
A key can optionally be locked to a set of source IPs or CIDR ranges. Requests from any other
address return 403 Request IP not in allowlist, even if the key and scopes are otherwise
valid. Configure the allowlist in the dashboard when creating the key.
Allowlist entries can be either plain IPs (203.0.113.7) or CIDR (203.0.113.0/24).Use this for tightly-scoped server-side keys — never for keys that legitimately roam (e.g. a laptop on a hotel network).
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
- Separate live and sandbox keys. Sandbox in dev/CI, live only in production. Pre-prod smoke tests should use a sandbox key.
- 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.