06 — Catálogo de eventos

Todo evento entregado por webhook comparte el mismo sobre:

{
  "id": "3f6a8c2e-1b5d-4f9a-8c6e-9f8c2a1e4b7d",
  "type": "transaction.created",
  "api_version": "v1",
  "created_at": "2026-05-14T18:32:09.104Z",
  "tenant_id": "7c1e4b2a-9f3d-4e8a-b6c5-2d1f0a9e8b7c",
  "livemode": true,
  "data": { "...": "específico de cada evento, ver abajo" }
}
CampoDescripción
idÚnico por evento y estable entre reintentos: úsalo para descartar duplicados
typeUno de los eventos de este catálogo
api_versionVersión del formato del evento, hoy v1
created_atMomento en que ocurrió el evento
tenant_idComercio al que pertenece el evento
livemodetrue si el evento viene de cobros reales; false si viene del modo de pruebas
dataContenido propio de cada evento, ver abajo

api_version viaja en cada evento (no solo en la URL de la API) para que puedas evolucionar tu manejo de payloads de forma independiente a cuándo actualizas la integración REST.

Cada entrega trae además las cabeceras X-Yallego-Event-Id (igual a id) y X-Yallego-Delivery-Id (la entrega de ese evento a ese endpoint, la misma en todos los intentos). Cuál usar para descartar duplicados está en 08 — Política de reintentos; cómo verificar la firma, en 07.

Un endpoint para varios comercios

tenant_id dice de qué comercio es cada evento, así que un mismo endpoint puede atender a todos los comercios que integras: registra la misma URL en cada uno y enruta por tenant_id, sin inventar una URL o un token por comercio. Es el identificador del comercio en Yallegó, el mismo que llega en connected del tiempo real (ver «Tiempo real» más abajo).

Cada registro tiene su propio secreto. Usa tenant_id del cuerpo solo para elegir con qué secreto verificar la firma, y confía en el resto del evento recién cuando la firma sea válida.

livemode lo decide el comercio, no la solicitud: hoy todos los comercios son reales y siempre llega true. Cuando exista el modo de pruebas, los eventos de un comercio de pruebas llegarán con false; descártalos en tu sistema de producción.

tenant_id y livemode se agregaron sin cambiar api_version: el resto del sobre no cambió y la firma se calcula igual. Si tu integración valida el cuerpo contra un esquema cerrado, acepta campos nuevos en el sobre.

transaction.created

Se dispara cuando un cobro se parsea correctamente desde una notificación capturada — el evento con mayor volumen, es el que mueve la mayoría de integraciones (conciliación, punto de venta, contabilidad). Lleva la transacción completa, con el mismo formato que GET /v1/transactions/{id}:

{
  "transaction": {
    "id": "9f8c2a1e-...",
    "wallet": { "code": "YAPE", "display_name": "Yape" },
    "sender_name": "JUAN CARLOS PEREZ R.",
    "amount": "35.50",
    "currency": "PEN",
    "security_code": "247",
    "approval_code": null,
    "status": "CAPTURED",
    "occurred_at": "2026-05-14T18:32:07.412Z",
    "confirmed_at": null,
    "confirmed_by": null,
    "confirmed_by_name": null,
    "disputed_at": null,
    "disputed_by": null,
    "disputed_by_name": null,
    "note": null,
    "device": { "id": "...", "label": "Celular caja principal" }
  }
}

En los tres eventos transaction.*, transaction es una foto del cobro tomada al despachar el evento (casi siempre, en el mismo instante en que ocurrió) y no cambia entre reintentos: si necesitas su estado de ahora, consulta GET /v1/transactions/{id}.

transaction.confirmed

Se dispara al llamar POST /v1/transactions/{id}/confirm, sea desde el panel o desde tu propia integración con transactions:write. Lleva la transacción completa, con el mismo formato que GET /v1/transactions/{id}:

{
  "transaction": {
    "id": "9f8c2a1e-...",
    "status": "CONFIRMED",
    "confirmed_at": "2026-05-14T18:40:00Z",
    "confirmed_by": "a1b2c3d4-...",
    "confirmed_by_name": "Rosa Quispe",
    "...": "resto de campos de la transacción"
  }
}

transaction.disputed

Análogo a transaction.confirmed, disparado por POST /v1/transactions/{id}/dispute. Incluye quién y cuándo reportó el cobro:

{
  "transaction": {
    "id": "9f8c2a1e-...",
    "status": "DISPUTED",
    "disputed_at": "2026-05-14T18:41:00Z",
    "disputed_by": "a1b2c3d4-...",
    "disputed_by_name": "Rosa Quispe",
    "...": "resto de campos de la transacción"
  }
}

confirmed_by y disputed_by (y sus *_name) son null cuando actuó una integración por clave de API en vez de un usuario del panel. En cobros reportados antes de que existiera el registro, disputed_at puede ser null.

device.offline

Se dispara cuando un dispositivo supera el umbral de 15 minutos sin heartbeat. Útil para alertar operativamente antes de perder cobros.

{ "device_id": "...", "label": "Celular caja principal", "last_seen_at": "2026-05-14T18:37:12Z" }

last_seen_at es null si el dispositivo nunca envió un heartbeat después de vincularse. La caída se revisa cada minuto, así que el evento puede llegar hasta un minuto después de cumplirse el umbral.

device.online

Se dispara cuando un dispositivo previamente OFFLINE vuelve a enviar un heartbeat.

{ "device_id": "...", "label": "Celular caja principal", "recovered_at": "2026-05-14T19:05:00Z" }

Cada device.offline tiene a lo sumo un device.online posterior, aunque lleguen varios heartbeats a la vez. Como cualquier entrega puede repetirse (ver reintentos), descarta duplicados por id.

Suscripción selectiva

Al crear o actualizar un webhook, subscribed_events acepta cualquier subconjunto de esta lista — un endpoint solo recibe los eventos que suscribió explícitamente, nunca todos por defecto.

Eventos retirados

notification.unmatched (una notificación de billetera que ningún parser reconoce) aparecía en versiones anteriores de este catálogo, pero nunca se emitió. Salió del catálogo hasta que exista:

  • Crear un webhook que lo incluya responde 400 VALIDATION_ERROR.
  • Al actualizar (PATCH /v1/webhooks/{id}) se acepta y se descarta, para que una integración que reenvía la lista que leyó antes no falle. Si la lista queda sin ningún otro evento, responde 400 VALIDATION_ERROR.
  • Los endpoints que lo tenían suscrito siguen funcionando con el resto de sus eventos; notification.unmatched ya no aparece en subscribed_events.

Cuando vuelva a emitirse lo anunciaremos aquí y tendrás que suscribirlo de nuevo.

Tiempo real

Los planes con acceso al canal de tiempo real (Comercio y Cadena) reciben los mismos hechos por Socket.IO, sin registrar una URL. Requiere una clave con el alcance realtime:subscribe:

import { io } from 'socket.io-client';

const socket = io('https://api.yallego.app', {
  path: '/v1/realtime',
  transports: ['websocket'],
  auth: { token: 'yk_live_...' },
});

socket.on('connected', ({ tenant_id, livemode }) =>
  console.log('Suscrito a', tenant_id, livemode ? '(real)' : '(pruebas)'),
);
socket.on('transaction.created', (event) => console.log('Cobro', event.amount));
socket.on('error', ({ code, message }) => console.error(code, message));

Cada evento llega como un objeto plano que siempre incluye tenant_id y livemode, con el mismo significado que en el sobre de los webhooks:

EventoPayload
connected{ session_id, tenant_id, livemode } — la suscripción ya está activa
transaction.created{ id, wallet_code, device_id, amount, currency, occurred_at, tenant_id, livemode }
transaction.confirmed{ transaction_id, confirmed_by, confirmed_at, tenant_id, livemode }
transaction.disputed{ transaction_id, disputed_at, tenant_id, livemode }
device.status_changed{ device_id, connectivity, changed_at, tenant_id, livemode }
error{ code, message } — se envía antes de cerrar la conexión
{
  "transaction_id": "9f8c2a1e-...",
  "disputed_at": "2026-05-14T18:41:00.000Z",
  "tenant_id": "7c1e4b2a-9f3d-4e8a-b6c5-2d1f0a9e8b7c",
  "livemode": true
}
  • connectivity es ONLINE u OFFLINE; acompaña a los webhooks device.online y device.offline. El paso a DEGRADED no genera evento.
  • confirmed_by es null cuando confirmó una integración por clave de API.
  • Los códigos de error son UNAUTHENTICATED (clave inválida o ausente), FORBIDDEN (plan sin tiempo real o clave sin realtime:subscribe) y SERVICE_UNAVAILABLE (no se pudo abrir el canal; reintenta).
  • El tiempo real no guarda eventos: si tu conexión se corta, al recibir de nuevo connected consulta GET /v1/transactions para ponerte al día. Para no perder ningún evento, usa webhooks.