Saltar al contenido principal

Eventos y webhooks

Próximamente

El contrato de eventos está publicado para diseño; la entrega de eventos todavía no está en servicio.

El CMS publica eventos cuando algo cambia: una compra autorizada, una recarga, un bloqueo, una decisión sobre una solicitud. Puedes recibirlos de dos formas:

ModoCómo funcionaCuándo usarlo
WEBHOOKFINORA hace POST a tu URL https con cada evento, firmadoQuieres reaccionar en segundos y puedes exponer un endpoint
PULLTú pides los eventos desde tu último cursor confirmadoNo puedes recibir conexiones entrantes, o necesitas orden estricto

Las suscripciones las administras con el alcance webhooks:write; la lectura de eventos exige events:read.

Tipos de evento​

TipoOrigenOpt-in
operacion.autorizada, operacion.rechazadaCompra, avance o preautorización con fase finalno
operacion.reversada, operacion.devueltaReverso o devoluciónno
cuenta.abonadaRecarga, pago o reembolsono
cuenta.ajustadaAjuste crédito o débitono
ajuste.decididoDecisión o vencimiento de un ajuste pendienteno
cuenta.ajuste_indexacionRevaluación UVC por cuentasí
indexacion.revaluacion_completadaCorrida de revaluación por lote, una por corrida y productosí
cuenta.interes_devengado, cuenta.comision_cobradaDevengo o comisiónno
tarjeta.bloqueada, tarjeta.desbloqueada, tarjeta.reportadaAcciones sobre el plásticono
tarjeta.limites_cambiadosTopes del plásticono
cuenta.linea_cambiadaCambio de línea aplicadono
solicitud_linea.decididaDecisión sobre una solicitud de líneano
solicitud_principal.decididaDecisión o anulación de un cambio de cuenta principalno
lote_limites.completadoFin de un loteno
estado_cuenta.emitidoEstado de cuenta emitidono

Puedes usar comodines de sufijo (operacion.*). Suscribirte a un tipo opt-in (por su nombre o con indexacion.*) exige además movements:adjustments; sin él, 403 ALCANCE_INSUFICIENTE. Pide los tipos opt-in por su nombre. Ver Movimientos y ajustes.

Crear una suscripción WEBHOOK​

curl https://api-cms.example.com/v1/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"nombre": "Cobranza",
"tipo": "WEBHOOK",
"tiposEvento": ["operacion.*", "cuenta.abonada"],
"url": "https://cliente.example.com/finora/webhooks"
}'
{
"subscriptionId": "7e6d5c4b-3a29-4180-9f7e-6d5c4b3a2918",
"nombre": "Cobranza",
"tipo": "WEBHOOK",
"tiposEvento": ["operacion.*", "cuenta.abonada"],
"url": "https://cliente.example.com/finora/webhooks",
"estado": "ACTIVA",
"cursorConfirmado": null,
"fallosConsecutivos": 0,
"creadaEn": "2026-10-20T12:00:00-04:00",
"secreto": {
"secretId": "whsec_2026_10",
"valor": "whsec_EJEMPLO_NO_REAL_c2VjcmV0by1kZS1lamVtcGxv",
"vigenteDesde": "2026-10-20T12:00:00-04:00"
}
}
  • La URL debe ser https y de un dominio de tu lista registrada; si no, 422 URL_NO_PERMITIDA. Las entregas salen por un proxy que bloquea rangos privados y no sigue redirecciones.
  • El secreto se muestra una sola vez (Cache-Control: no-store). Guárdalo en tu bóveda.

Qué recibe tu endpoint​

POST /finora/webhooks HTTP/1.1
Content-Type: application/json
webhook-id: e4a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b
webhook-timestamp: 1792519511
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{
"id": "e4a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b",
"seq": "c2VxOjEwMzA",
"tipo": "tarjeta.limites_cambiados",
"ocurrio": "2026-10-20T14:05:11-04:00",
"productCode": "210",
"datos": {
"cardId": "9b1f0e2d-3c4b-4a59-8e7f-6a5b4c3d2e10",
"accountId": null,
"operationId": null,
"jobId": null,
"requestId": null,
"detalle": { "maximoDiario": "500.00", "origen": "API" }
}
}

Los eventos son avisos con identificadores: si necesitas el estado completo, consúltalo con el GET correspondiente.

Verificar la firma​

La firma sigue Standard Webhooks: webhook-signature lleva una o más firmas v1,<base64> separadas por espacio, cada una un HMAC-SHA256 de webhook-id.webhook-timestamp.cuerpo con un secreto vigente. Durante una rotación llegan dos firmas: acepta el evento si cualquiera coincide.

import crypto from 'node:crypto';

/**
* secretos: los valores vigentes («whsec_» + llave en base64, como define Standard Webhooks);
* cuerpo: los bytes crudos recibidos.
*/
export function verificar({ cabeceras, cuerpo, secretos }) {
const id = cabeceras['webhook-id'];
const ts = Number(cabeceras['webhook-timestamp']);
if (!id || !Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; // ±5 min

const firmadas = String(cabeceras['webhook-signature'] ?? '')
.split(' ')
.filter((f) => f.startsWith('v1,'))
.map((f) => Buffer.from(f.slice(3), 'base64'));

return secretos.some((s) => {
const llave = Buffer.from(s.replace(/^whsec_/, ''), 'base64');
const esperada = crypto.createHmac('sha256', llave).update(`${id}.${ts}.`).update(cuerpo).digest();
return firmadas.some((f) => f.length === esperada.length && crypto.timingSafeEqual(f, esperada));
});
}

Reglas para tu receptor:

  1. Verifica la firma sobre el cuerpo crudo, antes de parsear el JSON.
  2. Rechaza un webhook-timestamp con más de 5 minutos de desfase.
  3. Deduplica por webhook-id: la entrega es al menos una vez y el mismo evento puede llegar varias veces.
  4. Responde cualquier 2xx rápido (acuse) y procesa en segundo plano.
  5. No asumas orden: cada evento lleva seq; si necesitas orden estricto, usa una suscripción PULL.

Reintentos, desactivación y replay​

  • GET /v1/subscriptions/{subscriptionId}/deliveries lista las entregas fallidas o en reintento.
  • POST /v1/subscriptions/{subscriptionId}/replay reenvía un rango de hasta 90 días atrás (422 REPLAY_FUERA_DE_VENTANA si es más antiguo). Responde 202 con un replayId:
curl https://api-cms.example.com/v1/subscriptions/7e6d5c4b-3a29-4180-9f7e-6d5c4b3a2918/replay \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"desde":"2026-10-18T00:00:00Z","hasta":"2026-10-19T00:00:00Z"}'
  • POST /v1/subscriptions/{subscriptionId}/secrets:rotate crea un secreto nuevo (visible una vez). El anterior sigue vigente 24 horas y, mientras tanto, cada entrega lleva las dos firmas.
  • DELETE /v1/subscriptions/{subscriptionId} desactiva la suscripción.

Suscripción PULL​

Crea la suscripción con "tipo": "PULL" (sin url) y lee en orden desde tu último cursor confirmado:

curl https://api-cms.example.com/v1/subscriptions/7e6d5c4b-3a29-4180-9f7e-6d5c4b3a2918/events \
-H "Authorization: Bearer $TOKEN"

Cuando hayas procesado y guardado una página, confirma su cursor; la siguiente lectura empezará después de él:

curl https://api-cms.example.com/v1/subscriptions/7e6d5c4b-3a29-4180-9f7e-6d5c4b3a2918/cursors \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"cursor":"c2VxOjEwNTA"}'
  • Confirmar un cursor por delante de lo entregado responde 422 CURSOR_ADELANTADO.
  • Si dos procesos confirman a la vez, uno recibe 409 CURSOR_CONCURRENTE: relee y continúa.
  • Para una lectura puntual sin suscripción existe GET /v1/events (alcance events:read).