Ir al contenido
FINORA Academy
SistemaFINORA Core

FINORA Core · Guía

Consumir el streaming de eventos del core

ImplementadoImplementado

Cada operación del core escribe su evento en el registro de eventos en la misma transacción. El streaming te lo entrega en orden y con una posición (el cursor). Guardando el último cursor que procesaste, puedes detener tu consumidor una hora o un día y retomar exactamente donde quedaste.

flowchart LR
  subgraph Core
    OB[("Registro de eventos<br/>orden estable")]
  end
  C["Tu consumidor"] -->|"1. leer desde mi cursor"| OB
  OB -->|"2. lote + cursor del último"| C
  C -->|"3. procesar"| DB[("Tu base")]
  C -->|"4. confirmar cursor"| OB

Tres reglas que conviene tener claras:

  • Orden estable. Los eventos salen ordenados por la transacción que los creó y su orden dentro de ella, y solo de transacciones ya confirmadas: un evento que confirma tarde no aparece «por detrás» de un cursor que ya entregaste.
  • Al menos una vez. Si tu consumidor cae entre procesar y confirmar, recibirás otra vez esos eventos. Deduplica por eid, el identificador estable de cada evento.
  • El cursor es opaco. Tiene la forma 48213-1052, pero no le busques significado ni lo construyas: úsalo tal como llega. BEGIN (o vacío) es antes del primer evento conservado.
TipoQué anunciametadata.aggregateType
AccEntryPostedUn asiento contabilizadoAccEntry
TransactionPostedUna transacción de clienteDatTransaction
AccountChangedUna cuenta de cliente creada o cambiadala cuenta
CustomerChangedUn cliente creado o cambiadoel cliente
Lectura libreSuscripción
RutaGET /v1/events?cursor&limit&eventTypesGET /v1/subscriptions/{id}/events?limit
Dónde vive tu cursorEn tu sistemaEn el core
ParaUna lectura puntual, una carga inicial, depurarUn consumidor permanente

Ambas exigen el alcance events:read. En el core es un permiso crítico, porque saca eventos del banco en masa: la credencial del gateway ante el core lo necesita de forma expresa.

Ventana de terminal
curl -s -D - "$GW/v1/events?cursor=BEGIN&limit=100&eventTypes=AccEntryPosted,TransactionPosted" \
-H "Authorization: Bearer $TOKEN"

limit va de 1 a 1000 (100 por defecto). La respuesta:

{
"cursor": "48213-1052",
"events": [
{
"eid": "0b7f3d2a-6c1e-4f8a-9d2b-5e4c3a1f0e9d",
"cursor": "48213-1052",
"occurredAt": "2026-09-19T14:05:12.481Z",
"eventType": "AccEntryPosted",
"metadata": { "aggregateType": "AccEntry", "aggregateId": "5a2e9c1b-3d4f-4a6b-8c7d-9e0f1a2b3c4d" },
"body": { "eventType": "AccEntryPosted", "eventVersion": "1", "payload": { "currency": "VES", "baseAmount": "116.00" } }
}
]
}

El cursor de arriba es el del último evento del lote; el gateway lo copia también en la cabecera Items-Next-Cursor. Para el siguiente lote, pásalo en ?cursor=. Con el lote vacío, el cursor no cambia: espera unos segundos y vuelve a leer.

  1. Crea la suscripción, una vez:

    Ventana de terminal
    curl -s -i "$GW/v1/subscriptions" -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{
    "owningApplication": "conciliador-banco-demo",
    "consumerGroup": "default",
    "eventTypes": ["AccEntryPosted"],
    "readFrom": "begin"
    }'

    Verás 201 con el id. La suscripción es única por aplicación, grupo y tipos de evento: repetir la misma petición devuelve 200 con la existente, así que tu consumidor puede crearla al arrancar sin miedo. readFrom es begin (por defecto, desde el primer evento conservado) o end (solo lo que ocurra desde ahora).

  2. Lee un lote. Siempre empieza en el último cursor confirmado; no se pasa cursor (si lo pasas: 400 CURSOR_NO_ADMITIDO):

    Ventana de terminal
    curl -s "$GW/v1/subscriptions/<ID>/events?limit=200" -H "Authorization: Bearer $TOKEN"

    Mientras no confirmes, la misma llamada devuelve los mismos eventos.

  3. Procesa el lote y guarda en tu base, en la misma transacción, los eid que procesaste.

  4. Confirma el cursor del lote:

    Ventana de terminal
    curl -s "$GW/v1/subscriptions/<ID>/cursors" -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" -d '{ "cursor": "48213-1052" }'

    Respuesta: {"committed": true, "committedCursor": "48213-1052"}. Confirmar un cursor igual o anterior al ya confirmado responde 200 con committed: false: no es un error, la confirmación es idempotente.

  5. Repite desde el paso 2. Con el lote vacío, espera unos segundos antes de volver a leer.

// Consumidor mínimo (Node 18+). TOKEN se renueva antes de sus 900 segundos.
async function consumir(gw, token, id, procesar) {
const h = { Authorization: `Bearer ${token()}`, 'Content-Type': 'application/json' };
for (;;) {
const lote = await (await fetch(`${gw}/v1/subscriptions/${id}/events?limit=200`, { headers: h })).json();
if (lote.events.length === 0) {
await new Promise((r) => setTimeout(r, 5000));
continue;
}
for (const e of lote.events) await procesar(e); // idempotente por e.eid
await fetch(`${gw}/v1/subscriptions/${id}/cursors`, {
method: 'POST', headers: h, body: JSON.stringify({ cursor: lote.cursor }),
});
}
}
Lo que vesPor quéQué hacer
400 CURSOR_INVALIDOEl cursor no tiene el formato que entregó el coreUsa el cursor tal como llegó
422 CORE_CURSOR_AHEAD_OF_STREAMConfirmaste un cursor posterior al último evento disponibleConfirma solo cursores que recibiste
409 CORE_CONCURRENT_COMMITDos procesos confirmaron a la vez sobre la misma suscripciónUn solo consumidor por suscripción; reintenta
404 CORE_EVENT_SUBSCRIPTION_NOT_FOUNDLa suscripción fue borradaCréala de nuevo; empezará según su readFrom
Lotes siempre vacíosRegistro de eventos apagado, o ningún evento de esos tiposPregunta al administrador del core

Administración → Suscripciones de eventos lista los consumidores y el último cursor que confirmó cada uno. Borrar una suscripción hace que su consumidor empiece de nuevo: hazlo solo si eso es lo que quieres.