Saltar al contenido principal

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:

ModoCuándo usarloAlcance
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.

Disponibilidad

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ámetroNotas
cursorOpaco. Vacío o BEGIN empieza en el primer evento conservado.
limitDe 1 a 1000.
eventTypesTipos 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.

Próximamente

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>" }
}'
CampoNotas
urlHTTPS, sin credenciales en la URL; no se admiten direcciones loopback ni link-local.
eventTypesTipos de evento o ["*"].
methodPOST (por defecto), PUT o PATCH.
headersCabeceras 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​

CabeceraContenido
X-Finora-Signaturet=<segundos epoch>,v1=<hmac-sha256 en hexadecimal>
X-Finora-Webhook-IdClave de idempotencia de la entrega; igual en todos los reintentos.
X-Finora-Event-TypeTipo del evento.
X-Finora-Delivery-AttemptNúmero de intento.
User-AgentFINORA-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).

Node.js
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));
}
Python
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ónRuta
Ver el webhook y el estado del circuitoGET /api/v1/ext/webhooks/{id}
Reactivar tras corregir tu receptorPOST /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 borrarGET, PUT, DELETE sobre /api/v1/ext/webhooks[/{id}]