Authentication
OAuth2 (OIDC) for apps, API keys for server-to-server integrations, bearer tokens.
LealUp supports two authentication mechanisms:
- OAuth2 (OIDC) — for apps acting on behalf of a user.
- API keys — for server-to-server integrations (scripts, backends, sync jobs).
Both issue bearer tokens sent in the Authorization header.
OAuth2 / OIDC
Use this flow when your integration acts on behalf of a specific user (e.g., an app showing LealUp data inside your product).
Setup (one-time)
- Admin → API → OAuth Apps → Create app.
- Configure:
- Name — visible to users during consent.
- Redirect URIs — one or more (e.g.,
https://yourapp.com/callback). - Requested scopes — see list below.
- You receive:
client_id— public.client_secret— keep secret (store in vault, never in client code).
Flow (Authorization Code with PKCE — required)
1. Redirect user to:
https://api.lealup.com/v1/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=customers:read playbooks:read&
state=RANDOM_NONCE&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256
2. User grants consent → redirect to your callback with ?code=AUTH_CODE
3. Exchange code for token:
POST https://api.lealup.com/v1/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=CODE_VERIFIER
4. Response:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "customers:read playbooks:read",
"tenant_id": "01HXYZ..."
}Using the token
curl https://api.lealup.com/v1/customers \
-H "Authorization: Bearer eyJ..."Refresh
Access tokens last 1h. Refresh tokens last 90 days and rotate on each use.
POST /v1/oauth/token
grant_type=refresh_token&
refresh_token=OLD_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRETResponse includes a new access_token + new refresh_token (the previous one is invalidated).
API keys
Use API keys for server-to-server integrations without a human user (cron jobs, sync scripts, inbound webhooks).
Create
Admin → API → API Keys → New key
Fields:
- Name — readable identifier ("data warehouse sync", "zendesk alerts").
- Scopes — what the key can do (see list).
- IP allowlist — optional, CIDR ranges.
- Expires — optional (90 days recommended).
You get:
lealup_sk_live_abc123def456...The key is shown only once. Copy it and store in vault / secrets manager.
Format
lealup_sk_live_*— production.lealup_sk_test_*— dev.
Use
Direct header:
curl https://api.lealup.com/v1/customers \
-H "Authorization: Bearer lealup_sk_live_abc123def456..."Rotation
- Create a new key.
- Deploy with the new key.
- Revoke the old one in Admin → API → API Keys → [key] → Revoke.
Best practice: rotate every 90 days, after any suspicion of leak, or when someone with access leaves the company.
Revoke
Immediate — Admin → API → API Keys → [key] → Revoke. Requests with a revoked key return 401.
Scopes
Scopes control what a token can do.
Format
{resource}:{action}, e.g., customers:read, events:write.
Full list
| Scope | Allows |
|---|---|
customers:read | GET customers and contacts |
customers:write | POST/PATCH customers and contacts |
customers:delete | DELETE customers (soft-delete) |
health:read | GET health scores |
health:write | POST recalculate, PATCH models |
playbooks:read | GET playbooks and tasks |
playbooks:write | POST/PATCH playbooks |
tasks:read | GET tasks |
tasks:write | PATCH tasks, complete tasks |
events:write | POST events (ingestion) |
events:read | GET events (debugging) |
webhooks:read | GET webhook subscriptions |
webhooks:write | POST/PATCH webhook subscriptions |
users:read | GET users |
users:write | POST invite, PATCH role (restricted) |
analytics:read | GET analytics endpoints |
admin:* | full admin access (only OAuth with admin role or key created by admin) |
Ask for the minimum needed. If you only read, don't ask for :write.
Auth errors
| Status | Code | Meaning |
|---|---|---|
401 | unauthenticated | no token sent, or malformed |
401 | token_expired | access token expired — use refresh |
401 | token_revoked | key or token was revoked |
403 | insufficient_scope | token is valid but lacks the required scope |
403 | ip_not_allowed | key has an allowlist and your IP is not on it |
429 | rate_limited | too many requests — see Retry-After header |
Example response:
{
"type": "https://api.lealup.com/errors/insufficient_scope",
"title": "Insufficient scope",
"status": 403,
"detail": "Required scope 'customers:write' but token only has 'customers:read'",
"instance": "/v1/customers",
"trace_id": "01HXYZ123ABC"
}Always include the trace_id in issues reported to support.
Security
- HTTPS only — HTTP requests redirect to HTTPS; some endpoints reject with
400. - Token expiration — access tokens are short (1h). Refresh tokens are longer (90 days) but rotate.
- API keys at rest: stored hashed (SHA-256 + salt). We cannot recover them.
- Audit log — every API-key use is logged with
actor_id,trace_id, endpoint, timestamp. - Anomaly detection — unusual usage spikes trigger an admin alert.
Identity vs authorization
- Authentication (who) — valid token.
- Authorization (what they can do) — scopes + user/key role.
A token can be valid (auth OK) but lack the scope for an action → 403, not 401.
End-to-end example (Python)
import httpx
BASE = "https://api.lealup.com"
KEY = "lealup_sk_live_..."
async def list_customers():
async with httpx.AsyncClient() as client:
r = await client.get(
f"{BASE}/v1/customers",
headers={"Authorization": f"Bearer {KEY}"},
params={"limit": 50, "status": "active"},
)
r.raise_for_status()
return r.json()End-to-end example (TypeScript with SDK)
import { LealUpClient } from "@lealup/sdk";
const client = new LealUpClient({
apiKey: process.env.LEALUP_API_KEY!,
});
const customers = await client.customers.list({
status: "active",
limit: 50,
});