Autenticación
La FE API usa un esquema simple de dos capas:
- Credenciales de larga duración (
apiKeyId+apiSecret) — emitidas en el panel de Digimart, perduran hasta que las revoques. - JWT de corta duración (1 hora) — obtenido haciendo
POST /logincon las credenciales del paso 1. Se usa en el headerAuthorization: Bearer <token>de cada llamada subsiguiente.
¿Por qué este patrón?
Tener una key estática que viaja en CADA request expone el secret en logs, tráfico de red, y caches. El JWT corto se firma en memoria y se rota cada hora, así un leak temporal se ventila solo.
El FE_API_JWT_SECRET del lado del servidor se puede rotar sin
invalidar tus credenciales — los JWTs en vuelo expiran en ≤1h. Tus
credenciales (apiKeyId + secret) se rotan independientemente desde el
panel de Digimart.
El apiKeyId
Formato: fak_ + 12 caracteres hexadecimales (ej. fak_a1b2c3d4e5f6).
Es público — no es sensible. Se puede loguear, mostrar en UI, embeber
en URLs internas. Equivale al "username" en el intercambio del /login.
El apiSecret
Formato: 64 caracteres hexadecimales (32 bytes random).
Es sensible. Trátalo como una contraseña:
- ✅ Guárdalo en una variable de entorno o secret manager (AWS Secrets Manager, Google Secret Manager, Vault).
- ✅ Restringe el acceso al archivo
.env(chmod 600). - ❌ NO lo comitees a git.
- ❌ NO lo loguees en archivos de aplicación.
- ❌ NO lo expongas en código del frontend del navegador — esto debe vivir EXCLUSIVAMENTE en tu backend.
Flujo de /login
POST /api/v1/fe/login
Request
{
"apiKeyId": "fak_a1b2c3d4e5f6",
"apiSecret": "abcd1234ef56..."
}Response (200)
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600,
"environment": "ecf",
"scopes": [
"invoices:write",
"invoices:read",
"usage:read",
"billing:read"
]
}Response (401)
{
"message": "Invalid credentials",
"statusCode": 401
}Para evitar timing attacks, retornamos el mismo Invalid credentials en
TODOS los modos de fallo (key inexistente, secret incorrecto, key
revocada, key expirada). No intentes inferir cuál fue el problema del
mensaje — revisa el panel de Digimart.
Usando el JWT
Una vez que tengas el accessToken, mándalo en el header Authorization
de cada request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...El servidor valida:
- Firma del JWT — debe estar firmado con
FE_API_JWT_SECRET. - Expiración —
expno debe estar en el pasado. - Scope — el JWT debe tener el scope requerido por el endpoint
(ej.
POST /invoicesrequiereinvoices:write). - Estado de la key — la
DgiiApiKeysubyacente debe seguiractive. Si la revocas, los JWTs nuevos no se emitirán, y los en vuelo expiran en máximo 30 segundos (refresh interno).
Scopes
Un scope es un permiso atómico. Los scopes asignados a una key se incluyen
en el JWT emitido al hacer /login:
| Scope | Permite |
|---|---|
invoices:write | POST /invoices |
invoices:read | GET /invoices, GET /invoices/:id |
usage:read | GET /usage |
billing:read | GET /billing/current-period |
Al crear una key puedes restringir sus scopes. Una key con solo
invoices:read podría usarse en un dashboard de monitoreo sin riesgo
de que emita nada.
Rotación de credenciales
Cuando necesites rotar (sospecha de leak, cambio de equipo, etc.):
- En el panel de Digimart, click "Nueva API key" → genera el nuevo par.
- Despliega el nuevo par a tu servidor.
- Verifica que el nuevo par funcione (haz una llamada de prueba).
- Click en el ícono 🗑️ junto a la key vieja → "Revocar".
- La key vieja deja de funcionar inmediatamente (cache invalidado en menos de 30 segundos).
Puedes tener N keys activas simultáneas. Crea una por entorno (producción, staging, test) o por servicio (api-server, cron-worker).
Errores comunes
| Error | Causa | Solución |
|---|---|---|
401 Missing bearer token | No mandaste el header Authorization o no usaste el prefijo Bearer | Agrega Authorization: Bearer <token> |
401 Invalid or expired token | JWT mal-firmado, expirado, o decodificable | Vuelve a hacer POST /login |
401 API key has been revoked | La key subyacente fue revocada o expiró | Crea una key nueva y rota credenciales |
403 Missing required scope(s): X | El JWT no tiene el scope que el endpoint requiere | Crea una key con scopes que incluyan los necesarios |
Lifetime del JWT
- Default: 1 hora.
- No es configurable desde tu lado — está fijo en el servidor.
- No hay refresh tokens. Cuando expire, vuelve a hacer
/login— toma ~200ms, no es costoso.
Patrón recomendado para servicios backend:
let cachedToken = null;
let cachedExpiry = 0;
async function getToken() {
if (cachedToken && Date.now() < cachedExpiry - 60_000) {
return cachedToken;
}
const resp = await fetch('/api/v1/fe/login', { /* ... */ });
const { accessToken, expiresIn } = await resp.json();
cachedToken = accessToken;
cachedExpiry = Date.now() + expiresIn * 1000;
return cachedToken;
}El - 60_000 te da un margen de 1 minuto contra clock skew.