# Autenticación

Llaves, scopes, firma de petición e IP permitidas.

Toda petición lleva tu llave en `Authorization: Bearer`. Las que mueven dinero llevan además una firma. En producción, la llave sólo funciona desde las IP que registraste.

## Llaves

Se crean en la pantalla Llaves del panel. El formato es `sk_live_fital_<id>_<secreto>_<verificador>` en producción y `sk_test_fital_…` en la cuenta de prueba. Al crear una llave ves también su **secreto de firma**, una sola vez. Guarda ambos en tu gestor de secretos.

| | Producción | Cuenta de prueba |
|---|---|---|
| Quién la crea | Propietario o admin, con login de hace menos de 5 minutos | Propietario, admin o desarrollador |
| Caduca a los | 180 días | 90 días |
| Lista de IP | Obligatoria | Opcional |

### Vencimiento

La pantalla Llaves muestra la fecha de cada llave. La pantalla Avisos te avisa 30, 7 y 1 días antes, sin correo. Una llave vencida responde `401 invalid_api_key` y ya no se puede rotar: tendrás que crear otra, con su lista de IP.

### Varias empresas

Un usuario del panel puede pertenecer a más de una empresa. En ese caso el panel muestra el selector **Empresa**: cambiar de empresa abre una sesión nueva, y el login reciente no viaja. Llaves, listas de IP, destinos de webhook y datos son de cada empresa. Por API no hay selector: la llave determina la empresa.

## Scopes

Cada llave tiene sólo los permisos que le des. La referencia indica el scope de cada operación.

| Scope | Operaciones |
|---|---|
| `balances:read` | [`GET /v1/balances`](/docs/operaciones/listar-saldos/), [`GET /v1/limits`](/docs/operaciones/consultar-limites/) |
| `beneficiaries:read` | [`GET /v1/beneficiaries`](/docs/operaciones/listar-beneficiarios/), [`GET /v1/beneficiaries/{id}`](/docs/operaciones/consultar-beneficiario/), [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/), [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/) |
| `beneficiaries:write` | [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/), [`DELETE /v1/beneficiaries/{id}`](/docs/operaciones/eliminar-beneficiario/) |
| `collections:read` | [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/), [`GET /v1/collections`](/docs/operaciones/listar-cobros/), [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/) |
| `collections:write` | [`POST /v1/collections`](/docs/operaciones/crear-cobro/), [`POST /v1/collections/estimate`](/docs/operaciones/estimar-cobro/) |
| `deposit_addresses:read` | [`GET /v1/deposit-addresses`](/docs/operaciones/listar-direcciones-de-deposito/), [`GET /v1/deposit-addresses/{id}`](/docs/operaciones/consultar-direccion-de-deposito/) |
| `deposit_addresses:write` | [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/) |
| `deposits:read` | [`GET /v1/deposits`](/docs/operaciones/listar-depositos/), [`GET /v1/deposits/{id}`](/docs/operaciones/consultar-deposito/) |
| `events:read` | [`GET /v1/events`](/docs/operaciones/listar-eventos/), [`GET /v1/events/{id}`](/docs/operaciones/consultar-evento/) |
| `movements:read` | [`GET /v1/movements`](/docs/operaciones/listar-movimientos/), [`GET /v1/movements/{id}`](/docs/operaciones/consultar-movimiento/), [`GET /v1/movements/{id}/receipt`](/docs/operaciones/descargar-recibo/) |
| `payments:read` | [`GET /v1/payments`](/docs/operaciones/listar-pagos/), [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) |
| `payments:write` | [`POST /v1/payments`](/docs/operaciones/crear-pago/), [`POST /v1/payments/{id}/cancel`](/docs/operaciones/cancelar-pago/) |
| `quotes:write` | [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/) |
| `accounts:read` | [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/) |
| `accounts:write` | [`POST /v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/) |
| `wallets:read` | [`GET /v1/wallets`](/docs/operaciones/listar-wallets/) |
| `webhooks:read` | [`GET /v1/webhook-endpoints`](/docs/operaciones/listar-destinos-de-webhook/) |

[`GET /v1/status`](/docs/operaciones/consultar-estado/) acepta cualquier llave válida. [`GET /v1/health`](/docs/operaciones/consultar-salud/) y [`GET /v1/meta/egress-ips`](/docs/operaciones/consultar-ip-de-salida/) son públicas.

## Firma de petición

Con los scopes `payments:write`, `beneficiaries:write`, `collections:write`, `deposit_addresses:write`, `accounts:write` el bearer solo no basta. Una llave robada sin el secreto de firma no saca dinero.

```text
X-Fital-Timestamp: <segundos unix>
X-Fital-Signature: v1=<hex>
```

La firma es HMAC-SHA256, usando como clave el secreto de firma tal como lo recibiste, sobre estas cinco líneas unidas por salto de línea:

```text
timestamp
MÉTODO
ruta y consulta, siempre desde /v1/
sha256 hexadecimal de los bytes del cuerpo
Idempotency-Key, o vacío si la operación no la lleva
```

La ruta es siempre la canónica, `/v1/...`, también en los simuladores: cambiar de ambiente no cambia tu código. Una firma vale 5 minutos. La clave de idempotencia va firmada.

### Vector de prueba

Comprueba tu implementación con este caso. El resultado debe coincidir exactamente. Todos sus datos son ficticios y están publicados a propósito: el secreto del vector no pertenece a ninguna llave y no sirve en ningún ambiente.

```bash
CUERPO='{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}'
SECRETO_FICTICIO=c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a
HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1)
FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' 1793800000 POST /v1/payments "$HASH" pago-factura-1042 \
  | openssl dgst -sha256 -hmac "$SECRETO_FICTICIO" -r | cut -d' ' -f1)
echo "v1=$FIRMA"
```

```js
import { createHash, createHmac } from 'node:crypto';
const cuerpo = '{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}';
const secreto_ficticio = 'c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a';
const hash = createHash('sha256').update(cuerpo).digest('hex');
const canon = ['1793800000', 'POST', '/v1/payments', hash, 'pago-factura-1042'].join('\n');
console.log(`v1=${createHmac('sha256', secreto_ficticio).update(canon).digest('hex')}`);
```

```python
import hashlib, hmac
cuerpo = b'{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}'
secreto_ficticio = b"c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a"
canon = "\n".join(["1793800000", "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), "pago-factura-1042"])
print("v1=" + hmac.new(secreto_ficticio, canon.encode(), hashlib.sha256).hexdigest())
```

```php
<?php
$cuerpo = '{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}';
$secreto_ficticio = 'c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a';
$canon = implode("\n", ['1793800000', 'POST', '/v1/payments', hash('sha256', $cuerpo), 'pago-factura-1042']);
echo 'v1=', hash_hmac('sha256', $canon, $secreto_ficticio), "\n";
```

Las operaciones firmadas sin cuerpo, como [`POST /v1/payments/{id}/cancel`](/docs/operaciones/cancelar-pago/), firman el hash de cero bytes. [`POST /v1/collections/estimate`](/docs/operaciones/estimar-cobro/) se firma con la última línea vacía.

## IP permitidas

En producción es obligatorio registrar las IP por llave. Ver [IP permitidas](/docs/seguridad/ip-permitidas/).

## Rotación

Rota cada llave en el panel antes de que venza. Rotar crea una llave nueva, con los mismos scopes y la misma lista de IP, y un secreto de firma nuevo, que se muestran una sola vez.

La llave anterior, con su secreto de firma, sigue operando 24 horas: despliega la nueva en ese plazo. Mientras tanto aparece **En gracia** y no se puede rotar otra vez.

Si la llave se filtró, no rotes: revócala. La revocación aplica a escrituras de inmediato; una lectura puede tardar hasta 30 segundos. Vale igual para llaves de prueba.

## Errores comunes

| Error | Significa |
|---|---|
| `401 invalid_api_key` | Llave mala, vencida o revocada. Ver [Vencimiento](#vencimiento) |
| `401 invalid_signature` | Falta la firma, es inválida o está vencida |
| `401 wrong_environment` | Llave de otro ambiente |
| `403 forbidden_scope` | Falta un scope |
| `403 ip_not_allowed` | Tu IP no está en la lista de la llave |
| `403 kyb_required` | Tu empresa no tiene aprobación |

Un `401` o `403` de acceso no dice si un pago ocurrió. Ver [Reintentos](/docs/recetas/reintentos/).

## Qué sigue

[Idempotencia](/docs/idempotencia/).
