Ir al contenido
FINORA Academy
SistemaFINORA Core

FINORA Core · Conceptos

Webhooks firmados del core: el core te avisa

ImplementadoImplementado

El problema: preguntar cada minuto «¿hay algo nuevo?»

Sección titulada «El problema: preguntar cada minuto «¿hay algo nuevo?»»

Tu conciliador necesita enterarse de cada asiento contable. Si pregunta al core cada minuto, el 99 % de las preguntas vuelven vacías y, aun así, se entera tarde. Si pregunta cada segundo, carga al core sin necesidad.

Con un webhook, tu sistema registra una URL y los tipos de evento que le interesan. Cuando ocurre uno, el core le hace un POST firmado. Tu sistema solo trabaja cuando hay algo que hacer.

Los avisos salen del registro de eventos del core (el outbox): el evento se escribe en la misma transacción que la operación que lo produce. Si el asiento se revierte por un error, el evento tampoco existe. Nunca recibes el aviso de algo que no ocurrió.

flowchart LR
  OP["Operación del core<br/>(un asiento, una transacción)"] -->|"mismo commit"| OB[("Registro de eventos")]
  OB --> D["Despachador<br/>(cada 2 s)"]
  D -->|"una entrega por suscripción"| E[("Entregas")]
  E -->|"POST firmado"| R["Tu receptor<br/>HTTPS"]
  R -->|"2xx"| E
TipoCuándo
AccEntryPostedEl core contabilizó un asiento
TransactionPostedSe registró una transacción de cliente
AccountChangedSe creó o cambió una cuenta de cliente
CustomerChangedSe creó o cambió un cliente

Una suscripción pide una lista de tipos (hasta 50) o * para todos. Recibe los eventos ocurridos desde que existe, no los anteriores.

Un POST (o PUT o PATCH, si la suscripción lo pide) con Content-Type: application/json:

{
"webhookId": "whd_7c1e4b0f2a9d4c8e9b3f6a1d2e5c7b90",
"eventId": "0b7f3d2a-6c1e-4f8a-9d2b-5e4c3a1f0e9d",
"eventType": "AccEntryPosted",
"occurredAt": "2026-09-19T14:05:12.481Z",
"cursor": "48213-1052",
"aggregateType": "AccEntry",
"aggregateId": "5a2e9c1b-3d4f-4a6b-8c7d-9e0f1a2b3c4d",
"data": {
"eventType": "AccEntryPosted",
"eventVersion": "1",
"payload": { "cia": "001", "currency": "VES", "baseAmount": "116.00", "conceptCode": "GL-MANUAL" }
}
}

data es el sobre del evento tal como lo escribió el core; su contenido depende del tipo.

CabeceraQué es
X-Finora-Signaturet=<segundos>,v1=<hex>: la firma. Verifícala siempre
X-Finora-Webhook-IdIdentificador de la entrega. Es el mismo en todos los reintentos: úsalo para no procesar dos veces
X-Finora-Event-TypeEl tipo de evento
X-Finora-Delivery-AttemptNúmero de intento: 1, 2, 3…
User-AgentFINORA-Webhooks/1.0
Las tuyasHasta 20 cabeceras propias de la suscripción (por ejemplo un Authorization de tu receptor). No pueden pisar las anteriores ni las de transporte
v1 = hex( HMAC-SHA256( secreto, t + "." + cuerpo ) )
  • t es el momento del envío, en segundos desde 1970. Cada intento lleva un t nuevo.
  • El secreto es uno por suscripción, empieza por whsec_ y se muestra una sola vez: al crear la suscripción o al rotarlo. El core lo guarda cifrado.
  • Tu receptor recalcula la firma sobre el cuerpo crudo, la compara en tiempo constante y rechaza un t que se aleje de su reloj más de su tolerancia (cinco minutos es una buena cifra). Así nadie puede reenviarte un aviso viejo.

Es el mismo esquema t / v1 de los avisos de FINORA Checkout: si ya lo implementaste allí, tu código sirve. Cómo hacerlo paso a paso, en Java y en Node: Recibir y verificar los webhooks del core.

Respuesta de tu receptorLo que hace el core
2xxEntregado (DELIVERED)
5xx, 408, 429, tiempo agotado, error de red o de TLSReintenta más tarde (FAILED con su próxima hora)
3xx y el resto de 4xxNo reintenta (DEAD): es un error de configuración del receptor. Las redirecciones no se siguen

La espera entre intentos se duplica: 30 s, 1 min, 2 min, 4 min, 8 min, 16 min y 32 min con los valores por defecto, con un tope de 6 horas. Tras 8 intentos la entrega queda DEAD. Cada intento espera hasta 5 s para conectar y 10 s por la respuesta: responde 2xx rápido y procesa después.

stateDiagram-v2
  [*] --> PENDING
  PENDING --> DELIVERED: 2xx
  PENDING --> FAILED: 5xx, 408, 429 o sin respuesta
  FAILED --> DELIVERED: 2xx en un reintento
  FAILED --> FAILED: falla otra vez
  FAILED --> DEAD: 8 intentos
  PENDING --> DEAD: 3xx o 4xx
  DEAD --> PENDING: reenvío manual

El orden no está garantizado entre entregas que se reintentan, y una entrega puede llegarte más de una vez. Por eso: deduplica por X-Finora-Webhook-Id y, si el orden importa, usa el cursor del cuerpo o lee el streaming de eventos.

Si tu receptor se cae, no tiene sentido seguir golpeándolo. Tras 5 fallos reintentables seguidos el circuito de esa suscripción se abre y el core deja de enviarle durante 5 minutos. Pasada la pausa manda una sola entrega de prueba: si sale bien, el circuito se cierra y la cola fluye; si falla, vuelve a abrirse. Un 4xx no cuenta como fallo: tu receptor está arriba y está rechazando, y abrir el circuito no lo arregla.

Las demás suscripciones no se enteran: cada una tiene su propio circuito.

  • Solo https.
  • Sin usuario ni clave dentro de la URL: para autenticarte ante tu receptor usa una cabecera propia.
  • Nunca localhost, direcciones de bucle local ni de enlace local (incluida la de metadatos de la nube). Se valida al guardar y otra vez antes de cada envío, por si cambia el DNS. Las redes privadas del banco sí se admiten.
Webhook (empuje)Streaming de eventos (tirón)
Quién iniciaEl coreTu sistema
NecesitasUn receptor HTTPS publicadoSolo salida hacia el gateway
OrdenNo garantizado entre reintentosEstricto, por cursor
Si tu sistema se caeReintentos y luego DEAD con reenvío manualRetomas desde tu último cursor confirmado
Bueno paraReaccionar rápido a un hechoReplicar o conciliar sin perder nada