Eventos y webhooks
El core registra cada hecho de negocio confirmado (un asiento contable, un movimiento, un cambio de cuenta) en un outbox de eventos. Puedes consumirlo de dos formas:
| Modo | Cuándo usarlo | Alcance |
|---|---|---|
| Lectura por cursor (pull) | Conciliación, réplicas, cargas por lotes; tú controlas el ritmo. | events:read |
| Webhook (push) | Reaccionar en cuanto ocurre algo. | webhook:read, webhook:write |
Las dos entregan al menos una vez: deduplica siempre por el identificador del evento.
La entrega depende de que la instalación tenga activos el outbox y el despachador de webhooks. Con el outbox apagado, los lotes llegan vacíos y los webhooks registrados no envían nada. Confírmalo con el banco.
Leer eventos por cursor
curl -s "https://api.banco.example/api/v1/ext/events?limit=100&eventTypes=AccEntryPosted,TransactionPosted" \
-H "X-Api-Key: $FINORA_API_KEY"
| Parámetro | Notas |
|---|---|
cursor | Opaco. Vacío o BEGIN empieza en el primer evento conservado. |
limit | De 1 a 1000. |
eventTypes | Tipos separados por coma. |
{
"cursor": "48213-1052",
"events": [
{
"eid": "0f6c2b8e-4d1a-4e7b-9c3f-5a8d2e1b7c40",
"cursor": "48213-1052",
"occurredAt": "2026-10-13T14:51:21-04:00",
"eventType": "AccEntryPosted",
"metadata": { "aggregateType": "AccEntry", "aggregateId": "9e2d4c6a-1b3f-4a5d-8e7c-0f1a2b3c4d5e" },
"body": { }
}
]
}
El cursor de la respuesta es el del último evento del lote; con un lote vacío repite el que pediste. body es el sobre del evento tal como lo registró el core.
El catálogo cerrado de tipos de evento y el esquema de cada body (especificación AsyncAPI) todavía no están publicados. AccEntryPosted y TransactionPosted son ejemplos de tipos existentes.
Suscripciones de consumidor
Una suscripción guarda en el servidor el cursor confirmado, para que puedas reanudar sin llevarlo tú.
Crear (única por aplicación, grupo de consumidores y tipos; repetirla devuelve la existente con 200):
curl -s -X POST https://api.banco.example/api/v1/ext/event-subscriptions \
-H "X-Api-Key: $FINORA_API_KEY" -H "Content-Type: application/json" \
-d '{
"owningApplication": "conciliador-erp",
"eventTypes": ["AccEntryPosted"],
"consumerGroup": "default",
"readFrom": "begin"
}'
readFrom es begin (por defecto, desde el primer evento conservado) o end (desde el último evento actual). eventTypes admite ["*"] para todos.
Leer el lote siguiente: GET /api/v1/ext/event-subscriptions/{id}/events?limit=100. Mientras no confirmes, la misma llamada devuelve los mismos eventos.
Confirmar el cursor procesado:
curl -s -X POST https://api.banco.example/api/v1/ext/event-subscriptions/1b2c3d4e-5f60-4a7b-8c9d-0e1f2a3b4c5d/cursors \
-H "X-Api-Key: $FINORA_API_KEY" -H "Content-Type: application/json" \
-d '{ "cursor": "48213-1052" }'
{ "committed": true, "committedCursor": "48213-1052" }
Un cursor igual o anterior al confirmado responde 200 con committed: false (la confirmación es idempotente). Un cursor posterior al último evento disponible responde 422 (cursor-ahead-of-stream).
Webhooks
Registrar un webhook
curl -s -X POST https://api.banco.example/api/v1/ext/webhooks \
-H "X-Api-Key: $CLAVE_INTEGRACION" -H "Content-Type: application/json" \
-d '{
"name": "conciliacion-erp",
"url": "https://erp.example.com/finora/webhooks",
"eventTypes": ["AccEntryPosted", "TransactionPosted"],
"method": "POST",
"headers": { "Authorization": "Bearer <token de tu receptor>" }
}'
| Campo | Notas |
|---|---|
url | HTTPS, sin credenciales en la URL; no se admiten direcciones loopback ni link-local. |
eventTypes | Tipos de evento o ["*"]. |
method | POST (por defecto), PUT o PATCH. |
headers | Cabeceras propias para tu receptor. No pueden pisar las X-Finora-* ni las de transporte. Sus valores nunca se devuelven. |
La respuesta 201 incluye secret, el secreto de firma. Se muestra una sola vez: guárdalo en tu gestor de secretos. Para rotarlo, reemplaza el webhook con PUT /api/v1/ext/webhooks/{id} y "rotateSecret": true.
Qué recibe tu endpoint
| Cabecera | Contenido |
|---|---|
X-Finora-Signature | t=<segundos epoch>,v1=<hmac-sha256 en hexadecimal> |
X-Finora-Webhook-Id | Clave de idempotencia de la entrega; igual en todos los reintentos. |
X-Finora-Event-Type | Tipo del evento. |
X-Finora-Delivery-Attempt | Número de intento. |
User-Agent | FINORA-Webhooks/1.0 |
El cuerpo es el sobre JSON del evento. Responde 2xx rápido y procesa en segundo plano. Un 3xx no se sigue: cuenta como fallo.
Verificar la firma
La firma es HMAC-SHA256(secreto, "<t>." + cuerpo) en hexadecimal minúsculo. Calcúlala sobre los bytes crudos del cuerpo, compárala en tiempo constante y rechaza sellos de tiempo fuera de tu tolerancia (por ejemplo, 5 minutos).
import crypto from 'node:crypto';
export function firmaValida(secreto, cabecera, cuerpoCrudo, toleranciaS = 300) {
const partes = Object.fromEntries(cabecera.split(',').map((p) => p.trim().split('=')));
const t = Number(partes.t);
if (!t || !partes.v1 || Math.abs(Date.now() / 1000 - t) > toleranciaS) return false;
const esperado = crypto.createHmac('sha256', secreto)
.update(`${t}.`).update(cuerpoCrudo).digest('hex');
return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
}
import hmac, hashlib, time
def firma_valida(secreto: str, cabecera: str, cuerpo_crudo: bytes, tolerancia_s: int = 300) -> bool:
partes = dict(p.strip().split("=", 1) for p in cabecera.split(","))
t, v1 = partes.get("t"), partes.get("v1")
if not t or not v1 or abs(time.time() - int(t)) > tolerancia_s:
return False
esperado = hmac.new(secreto.encode(), f"{t}.".encode() + cuerpo_crudo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, v1)
Reintentos y circuito
Las entregas fallidas se reintentan con espera creciente. Tras varios fallos seguidos, el webhook se pausa (circuito abierto) y deja de recibir hasta que lo reactives.
| Operación | Ruta |
|---|---|
| Ver el webhook y el estado del circuito | GET /api/v1/ext/webhooks/{id} |
| Reactivar tras corregir tu receptor | POST /api/v1/ext/webhooks/{id}:reset-circuit |
| Buscar entregas (por suscripción, estado, tipo y fechas) | POST /api/v1/ext/webhooks/deliveries:search |
Reenviar una entrega (mismo X-Finora-Webhook-Id) | POST /api/v1/ext/webhooks/deliveries/{id}:resend |
| Listar, reemplazar y borrar | GET, PUT, DELETE sobre /api/v1/ext/webhooks[/{id}] |