Autenticación
OAuth2 (OIDC) para apps, API keys para integraciones server-to-server, bearer tokens.
LealUp soporta dos mecanismos de autenticación:
- OAuth2 (OIDC) — para apps que actúan en nombre de un usuario.
- API keys — para integraciones server-to-server (scripts, backends, sync jobs).
Ambos emiten bearer tokens que se envían en el header Authorization.
OAuth2 / OIDC
Usa este flow cuando tu integración actúa en nombre de un usuario específico (ej. una app que muestra datos de LealUp dentro de tu producto).
Setup (una vez)
- Admin → API → OAuth Apps → Crear app.
- Configura:
- Name — visible a usuarios durante el consent.
- Redirect URIs — uno o más (ej.
https://tuapp.com/callback). - Scopes solicitados — ver lista abajo.
- Recibes:
client_id— público.client_secret— mantén secreto (almacena en vault, nunca en código cliente).
Flow (Authorization Code with PKCE — obligatorio)
1. Redirect user a:
https://api.lealup.com/v1/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://tuapp.com/callback&
scope=customers:read playbooks:read&
state=RANDOM_NONCE&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256
2. Usuario da consent → redirect a tu callback con ?code=AUTH_CODE
3. Intercambia code por 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://tuapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=CODE_VERIFIER
4. Respuesta:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "customers:read playbooks:read",
"tenant_id": "01HXYZ..."
}Usar el token
curl https://api.lealup.com/v1/customers \
-H "Authorization: Bearer eyJ..."Refresh
Los access tokens duran 1h. El refresh token dura 90 días y rota en cada uso.
POST /v1/oauth/token
grant_type=refresh_token&
refresh_token=OLD_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRETRespuesta incluye nuevo access_token + nuevo refresh_token (el anterior se invalida).
API keys
Usa API keys para integraciones server-to-server sin usuario humano (cron jobs, sync scripts, webhooks entrantes).
Crear
Admin → API → API Keys → Nueva key
Campos:
- Name — identificador legible ("data warehouse sync", "alertas zendesk").
- Scopes — qué puede hacer la key (ver lista).
- IP allowlist — opcional, CIDR ranges.
- Expires — opcional (recomendado 90 días).
Obtienes:
lealup_sk_live_abc123def456...La key se muestra una sola vez. Copiala y almacénala en vault/secrets manager.
Formato
lealup_sk_live_*— production.lealup_sk_test_*— dev.
Usar
Header directo:
curl https://api.lealup.com/v1/customers \
-H "Authorization: Bearer lealup_sk_live_abc123def456..."Rotación
- Crea una key nueva.
- Deploy con la nueva key.
- Revoca la vieja en Admin → API → API Keys → [key] → Revocar.
Buena práctica: rotar cada 90 días, después de cualquier suspicion de leak, o cuando alguien con acceso sale de la empresa.
Revocar
Inmediato — Admin → API → API Keys → [key] → Revocar. Requests con la key revocada devuelven 401.
Scopes
Los scopes controlan qué puede hacer un token.
Formato
{recurso}:{acción}, ej. customers:read, events:write.
Lista completa
| Scope | Permite |
|---|---|
customers:read | GET customers y contactos |
customers:write | POST/PATCH customers y contactos |
customers:delete | DELETE customers (soft-delete) |
health:read | GET health scores |
health:write | POST recalculate, PATCH modelos |
playbooks:read | GET playbooks y tareas |
playbooks:write | POST/PATCH playbooks |
tasks:read | GET tasks |
tasks:write | PATCH tasks, complete tasks |
events:write | POST events (ingesta) |
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 (restringido) |
analytics:read | GET analytics endpoints |
admin:* | acceso completo admin (solo OAuth con rol admin o key creada por admin) |
Pide el mínimo necesario. Si solo vas a leer, no pidas :write.
Errores de auth
| Status | Código | Significado |
|---|---|---|
401 | unauthenticated | no se envió token, o está malformado |
401 | token_expired | access token expiró — usa refresh |
401 | token_revoked | key o token fue revocado |
403 | insufficient_scope | el token es válido pero no tiene el scope necesario |
403 | ip_not_allowed | key tiene allowlist y tu IP no está |
429 | rate_limited | exceso de requests — ver header Retry-After |
Ejemplo de respuesta:
{
"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"
}Incluye siempre el trace_id en issues reportados a soporte.
Seguridad
- HTTPS only — requests a HTTP redireccionan a HTTPS; algunos endpoints rechazan con
400. - Token expiration — access tokens son cortos (1h). Refresh tokens largos (90 días) pero rotan.
- API keys en reposo: almacenadas hasheadas (SHA-256 + salt). Nosotros no podemos recuperarlas.
- Audit log — todo uso de API keys queda en el audit log con
actor_id,trace_id, endpoint, timestamp. - Anomaly detection — picos de uso inusuales disparan alerta al admin.
Identidad vs autorización
- Autenticación (quién) — el token válido.
- Autorización (qué puede hacer) — scopes + rol del usuario/key.
Un token puede ser válido (auth OK) pero no tener el scope para una acción → 403, no 401.
Ejemplo end-to-end (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()Ejemplo end-to-end (TypeScript con 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,
});