# Llaves root y rotación (/docs/autenticacion/llaves-root)

Si estás incrustando Facturia en tu propio producto — un SaaS vertical, un ERP, un estudio contable —
vas a querer mintear una llave por empresa cliente sin que un humano abra un panel. Eso es un flujo
de primera clase en la v1.

## La llave root [#la-llave-root]

Los endpoints de gestión de llaves se autentican con una **llave root** (`sk_live_root_…` /
`sk_test_root_…`), portadora del scope `keys:write`. La mintea una **sesión de cuenta** —la única
credencial que puede— con `POST /dashboard/api_keys {"root": true}`; ni el panel ni un humano
abriendo una pantalla son un requisito, es la misma llamada que haría tu propio backend en el
arranque en frío:

```bash
curl -sS https://api.facturia.cl/dashboard/api_keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Facturia-Mode: live" \
  -H "Content-Type: application/json" \
  -d '{"name": "root — arranque en frío", "root": true}'
```

Sin `scopes` ni `empresas`: una llave root los lleva fijos (`["keys:write"]`, sin empresas) y
mandarlos igual a `["keys:write"]` no es error, mandar cualquier otra cosa sí lo es —
`400 invalid_field`. Una llave —root u ordinaria— que pide `{"root": true}` recibe
`403 privilege_escalation`: solo una sesión de cuenta está por encima de la delegación.

Una llave root no puede emitir, no puede leer documentos y no puede tocar datos tributarios. Su
único poder es mintear y revocar otras llaves. Ten exactamente una, guárdala donde guardas las
credenciales de tu base de datos, y nunca la despliegues a un runtime de edge.

## Mintear [#mintear]

```bash
curl -sS https://api.facturia.cl/v1/api_keys \
  -H "Authorization: Bearer $FACTURIA_ROOT_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "cliente — Comercial Andina",
    "mode": "live",
    "scopes": ["documents:read", "documents:write", "received:read"],
    "empresas": ["76543212-K"]
  }'
```

```json
{
  "id": "key_01K2QWB7T4N9",
  "object": "api_key",
  "name": "cliente — Comercial Andina",
  "mode": "live",
  "secret": "sk_live_qT8vN2wLmR5xKpJ7hYdCbA3s",
  "last4": "bA3s",
  "scopes": ["documents:read", "documents:write", "received:read"],
  "empresas": ["76543212-K"],
  "created_at": "2026-08-14T13:12:40Z"
}
```

`secret` se devuelve **exactamente una vez**. Como esta llave nombra una sola empresa, es un token de
empresa: entrégasela al sistema de ese cliente y no podrá direccionar nada más.

## Las tres barreras [#las-tres-barreras]

Se aplican en el servidor, no en tu código:

**Una llave minteada nunca puede alcanzar más que quien la mintea.** Sus `empresas` deben ser un
subconjunto de la lista blanca de la llave minteadora, y sus `scopes` un subconjunto de los suyos.
Una llave root con `["*"]` puede mintear cualquier cosa de la cuenta; una restringida a tres RUT solo
puede producir llaves dentro de esos tres. Las violaciones devuelven `403 privilege_escalation`
nombrando el RUT o el scope ofensor.

**El modo se hereda.** Una llave `sk_test_root_` solo mintea llaves de test. No hay camino de una
credencial de test a una live.

**Una llave root no puede mintear llaves root.** La delegación es de un nivel de profundidad, por
diseño. Si necesitas una segunda llave root, mintéala con una sesión de cuenta
(`POST /dashboard/api_keys {"root": true}`), como arriba.

## Listar, rotar, revocar [#listar-rotar-revocar]

```bash
# Listar (solo metadatos — los secretos no son legibles, ni siquiera por una llave root)
curl https://api.facturia.cl/v1/api_keys -H "Authorization: Bearer $FACTURIA_ROOT_KEY"

# Rotar: emite un secreto de reemplazo; el anterior sigue funcionando 24 h,
# así que puedes desplegar sin corte
curl -X POST https://api.facturia.cl/v1/api_keys/key_01K2QWB7T4N9/roll \
  -H "Authorization: Bearer $FACTURIA_ROOT_KEY"

# Revocar de inmediato
curl -X DELETE https://api.facturia.cl/v1/api_keys/key_01K2QWB7T4N9 \
  -H "Authorization: Bearer $FACTURIA_ROOT_KEY"
```

Una llave ordinaria (no root) que llame a `GET /v1/api_keys` se ve solo a sí misma — útil como health
check, inútil como inventario.

Todo esto queda auditado: `api_key.created`, `api_key.rolled` y `api_key.revoked` caen en el libro de
eventos con el id de la llave que las minteó.

> **Rotar no es revocar**
>
> `POST /v1/api_keys/{key_id}/roll` deja el secreto anterior vivo 24 horas para que puedas desplegar
> sin ventana de caída. Si estás rotando **porque una llave se filtró**, rota y después revoca — o
> revoca directamente y asume el corte. Las 24 horas son una comodidad de despliegue, no una respuesta
> a incidentes.

## El arranque en frío [#el-arranque-en-frío]

Todo lo anterior necesita una llave root, y una cuenta nueva no tiene ninguna. La salida es la
**sesión de cuenta**: el mismo JWT de una hora que devuelve `POST /account/login`.

```bash
# 1. Registrarte (o iniciar sesión). No se necesita navegador: es una llamada normal.
TOKEN=$(curl -sS https://api.facturia.cl/account/register \
  -H "Content-Type: application/json" \
  -d '{"email":"tu@empresa.cl","password":"..."}' | jq -r .data.token)

# 2. Mintear la primera llave con la sesión, no con una llave que todavía no tienes.
curl -sS https://api.facturia.cl/dashboard/api_keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Facturia-Mode: test" \
  -H "Content-Type: application/json" \
  -d '{"name":"primera llave","scopes":["documents:read","documents:write","empresas:read","empresas:write","webhooks:read","webhooks:write"],"empresas":["*"]}'
```

Esos seis son el **set de arranque**, el mismo en las tres superficies que lo publican (esta página,
el [quickstart](/docs/empezar/quickstart) y el contrato del arranque en frío). Es el mínimo con el
que se llega desde acá hasta un documento emitido: crear la empresa pide `empresas:write` y suscribir
un webhook pide `webhooks:write`, así que una llave con solo los dos de `documents:` muere en el
paso siguiente con `403 insufficient_scope`. Ver [Scopes](/docs/autenticacion/scopes).

Para dejar de sostener la contraseña de un humano, la sesión mintea una **llave root** en la misma
puerta:

```bash
curl -sS https://api.facturia.cl/dashboard/api_keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Facturia-Mode: test" \
  -H "Content-Type: application/json" \
  -d '{"name":"root de integracion","root":true}'
# → {"id":"key_…","root":true,"scopes":["keys:write"],"empresas":["*"],"secret":"sk_test_root_…"}
```

La root **hereda el alcance de la sesión** (`["*"]`: toda empresa de la cuenta, presentes y futuras).
Ese alcance es lo que la hace útil: la barrera de subconjunto compara las `empresas` de la llave
minteada contra las de quien mintea, así que una root que no alcanzara ninguna empresa no podría
producir ninguna llave que alcance alguna. Ella misma no puede usarlo — su único scope es
`keys:write` —; lo usan las llaves que mintea. `empresas` en un mint root puede ir ausente, `[]` o
`["*"]`; una lista más estrecha es `400 invalid_field` (se acota la llave minteada, no la root).

`/dashboard/*` es una superficie deliberadamente **estrecha**: empresas, documentos, documentos
recibidos, consumo, llaves de API y certificados, y nada más. En particular, no puede emitir.

### Si se perdió la contraseña [#si-se-perdió-la-contraseña]

La sesión es la única puerta a la primera llave, así que perder la contraseña dejaba a una persona
sin ninguna. Hay recuperación:

```bash
# 1. Pedir el enlace. SIEMPRE responde 202, exista o no la cuenta: una respuesta
#    distinta para una dirección desconocida sería una lista de clientes pública.
curl -sS https://api.facturia.cl/account/password/reset_request \
  -H "Content-Type: application/json" -d '{"email":"tu@empresa.cl"}'
# → 202 {"object":"password_reset_request","status":"accepted"}

# 2. Con el token del correo (vive 60 minutos, un solo uso):
curl -sS https://api.facturia.cl/account/password/reset \
  -H "Content-Type: application/json" -d '{"token":"...","password":"..."}'
# → 200 {"object":"account","id":"...","email":"..."}
```

Pedir un enlace nuevo invalida los anteriores, y cambiar la contraseña invalida los que queden. Un
token que no sirve responde `400 password_reset_token_invalid` sin decir cuál de las cuatro razones
fue: distinguirlas sería un oráculo para adivinar una credencial, y el remedio es el mismo — pedir
otro enlace.

`POST /account/login` y `POST /account/register` reportan ambos `emailVerified`, así que un cliente
puede mostrar el estado sin una segunda llamada. Mintear una llave `sk_live_` desde una cuenta sin
verificar devuelve `403 email_verification_required`; el modo test no se ve afectado.
