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" }
}
| Campo | Descripción |
|---|---|
id | Único por evento y estable entre reintentos: úsalo para descartar duplicados |
type | Uno de los eventos de este catálogo |
api_version | Versión del formato del evento, hoy v1 |
created_at | Momento en que ocurrió el evento |
tenant_id | Comercio al que pertenece el evento |
livemode | true si el evento viene de cobros reales; false si viene del modo de pruebas |
data | Contenido 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, responde400 VALIDATION_ERROR. - Los endpoints que lo tenían suscrito siguen funcionando con el resto de sus
eventos;
notification.unmatchedya no aparece ensubscribed_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:
| Evento | Payload |
|---|---|
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
}
connectivityesONLINEuOFFLINE; acompaña a los webhooksdevice.onlineydevice.offline. El paso aDEGRADEDno genera evento.confirmed_byesnullcuando confirmó una integración por clave de API.- Los códigos de
errorsonUNAUTHENTICATED(clave inválida o ausente),FORBIDDEN(plan sin tiempo real o clave sinrealtime:subscribe) ySERVICE_UNAVAILABLE(no se pudo abrir el canal; reintenta). - El tiempo real no guarda eventos: si tu conexión se corta, al recibir de
nuevo
connectedconsultaGET /v1/transactionspara ponerte al día. Para no perder ningún evento, usa webhooks.