# Webhooks -- Vision General

Los webhooks permiten que su aplicacion reciba notificaciones en tiempo real sobre eventos en la plataforma Monetarie. Cuando ocurre un evento, Monetarie envia un HTTP POST a la URL registrada.

## Como Funciona

1. [Registre una URL de webhook](/es/webhooks-register) en su cuenta
2. Cuando ocurra un evento (ej: PIX recibido), Monetarie envia un HTTP POST a su URL
3. Su aplicacion procesa la notificacion y responde con estado `2xx` (200, 201 o 204)

## Eventos Disponibles

Monetarie entrega eventos de **PIX**, **TEF entre cuentas Monetarie** y prueba operacional de webhook. Otros productos (boleto, cuentas y STA) no estan en alcance. Cualquier intento de suscribir eventos fuera de la tabla abajo es rechazado con `events: contains invalid events: ...`.

| Evento | Status body | Descripcion | Disparo |
|--------|-------------|-------------|---------|
| `pix.charge.created` | `created` | QR code generado o cash-in iniciado | **Activo** |
| `pix.charge.paid` | `paid` | PIX recibido y liquidado | **Activo** |
| `pix.charge.expired` | `expired` | QR code expiro sin pago | **Activo** |
| `pix.charge.cancelled` | `cancelled` | QR code cancelado antes del pago | Registrado, aun no disparado |
| `pix.payout.queued` | `queued` | PIX enviado encolado por limite operacional. Retry automatico con TTL maximo de 2h | **Activo** |
| `pix.payout.processing` | `processing` | PIX enviado, aguardando confirmacion BACEN | **Activo** |
| `pix.payout.held` | `processing` | PIX enviado retenido para analisis en el agente de liquidacion (autorizacion/anti-fraude). Liquida o rechaza despues - no reenviar | **Activo** |
| `pix.payout.confirmed` | `settled` | PIX enviado y confirmado (**terminal**) | **Activo** |
| `pix.payout.failed` | `rejected` | PIX enviado rechazado por el SPI (**terminal**) | **Activo** |
| `pix.payout.returned` | `returned` | PIX enviado devuelto | **Activo** |
| `pix.refund.requested` | `requested` | Solicitud de devolucion recibida (infraccion BACEN); bloqueo cautelar creado en el saldo del cliente | **Activo** |
| `pix.refund.completed` | `settled` / `completed` | Analisis de la defensa finalizado y devolucion ejecutada (o liberada) | **Activo** |
| `pix.refund.failed` | `failed` | Devolucion rechazada por el SPI/liquidante (**terminal**); incluye `reason_code` y `reason_description` | **Activo** |
| `pix.return.received` | `settled` | Devolucion PIX recibida (credito) | **Activo** |
| `pix.infraction.created` | `ACKNOWLEDGED` | Infraccion PIX reportada por la contraparte via BACEN DICT; puede exigir analisis y defensa MED | **Activo** |
| `pix.infraction.resolved` | `CLOSED` / `CANCELLED` | Infraccion resuelta por decision final o cancelacion de la contraparte | **Activo** |
| `pix.infraction.defense_submitted` | `defense_submitted` | Defensa enviada por el merchant (portal o API); aguarda analisis BACEN | **Activo** |
| `tef.transfer.sent` | `settled` | TEF entre cuentas Monetarie liquidada para la cuenta de origen | **Activo** |
| `tef.transfer.received` | `settled` | TEF entre cuentas Monetarie liquidada para la cuenta de destino | **Activo** |
| `tef.transfer.failed` | `failed` | TEF entre cuentas Monetarie rechazada o no liquidada | **Activo** |
| `webhook.test` | `test` | Prueba manual. **Disponible solo via Admin/Merchant portal** - la External API no expone endpoint para disparar prueba | Disparo manual (no External API) |

::: warning `pix.charge.cancelled` aun no es disparado
El evento esta en el enum y puede ser suscrito, pero el sistema no posee flujo de cancelacion de QR code hoy. Si lo suscribe, el `POST /webhooks` responde 201 normalmente - pero ninguna notificacion llegara. Continue monitoreando `pix.charge.expired` para el ciclo natural de vida del QR.
:::

## Seguridad

Cada notificacion incluye headers de seguridad e identificacion para validacion:

| Header | Descripcion |
|--------|-------------|
| `X-Monetarie-Signature` | Firma **HMAC-SHA256** del payload (prefijo `sha256=`). En casos raros (webhook registrado sin `secret`), el valor literal es `unsigned` - vea nota abajo |
| `X-Monetarie-Timestamp` | Unix timestamp en segundos del envio |
| `X-Monetarie-Event-Id` | UUID unico de la delivery (para deduplicacion) |
| `X-Monetarie-Event-Type` | Tipo del evento (ej: `pix.charge.paid`) |
| `Content-Type` | Siempre `application/json` |
| `User-Agent` | Siempre `Monetarie-Webhook/1.0` - use para whitelisting en firewalls/WAF |

::: warning Signature `unsigned` cuando webhook no tiene secret
Si el webhook fue registrado sin campo `secret` en un registro legacy, el header `X-Monetarie-Signature` puede venir como `unsigned`. Esto deshabilita la validacion HMAC de su lado. Si recibe `unsigned`, registre un nuevo webhook con `secret` explicito y remueva el antiguo.
:::

::: tip SHA256 en webhooks vs SHA512 en la API
La API usa **HMAC-SHA512** para autenticar solicitudes que usted envia. Los webhooks enviados por Monetarie usan **HMAC-SHA256** en la firma `X-Monetarie-Signature`. Son algoritmos diferentes -- cada uno en su contexto.
:::

### Validando la Firma

Valide la firma para garantizar que la notificacion fue enviada por Monetarie:

```javascript
const crypto = require('crypto');

function validateWebhook(rawBody, timestamp, signature, secret) {
  // rawBody is the RAW request body string (before any JSON parse)
  // timestamp is unix seconds (e.g., 1712160000)
  const message = `${timestamp}.${rawBody}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
```

::: danger Use el body RAW, no re-serializado
Usted **debe** usar el cuerpo exacto de la solicitud HTTP como los bytes llegaron a su aplicacion. Si hace `JSON.parse` y despues `JSON.stringify`, los bytes resultantes **no seran** identicos a los que Monetarie uso para firmar, y la validacion fallara.

En Express/Node: use `express.raw({ type: 'application/json' })` o guarde el body antes de cualquier middleware de parse.

En otros frameworks: configure para capturar el raw body antes del middleware JSON.
:::

::: warning Ordenacion de claves en WEBHOOKS: NO es necesaria
Para **validacion de webhooks** (HMAC-SHA256) usted **NO necesita ordenar las claves** - use el body raw como fue recibido en el HTTP request de Monetarie.

⚠️ **Atencion - diferencia vs envio de solicitudes**: En la [firma HMAC-SHA512 de SOLICITUDES que usted envia](/es/hmac), la ordenacion alfabetica de las claves **ES obligatoria** (el servidor Monetarie reordena antes de validar). No confunda los dos escenarios:
- **Webhook recibido (HMAC-SHA256)**: valide el body raw sin reordenar
- **Request enviada (HMAC-SHA512)**: ordene sus claves alfabeticamente antes de firmar
:::

::: warning Valide siempre
Nunca procese un webhook sin validar la firma. Esto protege contra solicitudes falsificadas.

Adicionalmente, valide que el `X-Monetarie-Timestamp` este dentro de **± 5 minutos** de la hora actual (proteccion anti-replay) y deduplique eventos por `X-Monetarie-Event-Id` (proteccion contra retries).
:::

## Orden de los Eventos

Los eventos de un mismo pago pueden llegar a su endpoint **fuera de orden** en ventanas cortas (las entregas HTTP son independientes y un PIX puede liquidarse en menos de un segundo). Trate los estados segun la tabla:

| Tipo de evento | Semantica |
|---|---|
| `*.processing` / intermedios | Informativo. **Nunca** debe sobrescribir un estado terminal ya recibido. |
| `*.confirmed`, `*.paid`, `*.completed`, `*.failed` | **Terminal (absorbente)**: una vez recibido, ignore cualquier evento intermedio posterior del mismo pago. |

Reglas practicas para su consumidor:

1. Aplique transiciones de estado por el **contenido** del evento (campo `status`), no por el orden de llegada.
2. Un pago en estado terminal nunca retrocede: descarte un `processing` recibido despues de `confirmed`/`failed`.
3. Deduplique por `X-Monetarie-Event-Id` (los reintentos reenvian el mismo evento con el mismo id).

## Retry Policy

Si su URL retorna un estado diferente de `2xx` (o timeout despues de 30 s), Monetarie realiza hasta **8 intentos** con backoff exponencial. El tiempo total entre el primer y el octavo intento es aproximadamente **7h45min**:

| Intento | Delay desde intento anterior | Tiempo acumulado |
|---------|------------------------------|------------------|
| 1o | - (inmediato) | ~50-200 ms |
| 2o | 30 segundos | ~30 s |
| 3o | 2 minutos | ~2,5 min |
| 4o | 10 minutos | ~12,5 min |
| 5o | 30 minutos | ~42,5 min |
| 6o | 1 hora | ~1,75 h |
| 7o | 2 horas | ~3,75 h |
| 8o | 4 horas | ~7,75 h |

Despues de 8 intentos sin exito, el `webhook_delivery` es marcado con estado `failed` y no es reenviado automaticamente. Usted puede solicitar replay manual al soporte Monetarie proporcionando el `X-Monetarie-Event-Id` (o tener un operador con acceso admin replayar por el portal).

::: tip Estado de una delivery
Cada delivery pasa por los estados: `pending` (creada, aguardando entrega) → `delivered` (2xx recibido) O `failed` (8 intentos agotados) O `expired` (replay protection).

**Sobre `expired`:** cuando un intento de entrega queda demasiado antiguo antes del envio, puede ser descartado y marcado como `expired`. Esto impide que reprocesamientos tardios disparen notificaciones antiguas. Replays manuales solicitados al soporte Monetarie siguen un flujo controlado y pueden reenviar el evento al cliente.
:::

::: tip Durabilidad
Antes de intentar la primera entrega, el evento se persiste para retry. Si ocurre una falla durante la entrega, el sistema retoma automaticamente en el proximo retry - ningun evento es perdido.
:::

::: tip Idempotencia
Su aplicacion debe ser **idempotente**: si recibe el mismo evento mas de una vez (identificado por el `X-Monetarie-Event-Id`), debe procesarlo sin duplicar efectos.
:::

### Replay manual via admin

Si una delivery fallo y usted necesita re-enviar, el equipo Monetarie puede ejecutar replay manual via admin dashboard. Contacte al soporte con el `event_id` de la delivery.

### Entregas duplicadas (race condition conocida)

El sistema puede usar mas de un camino de entrega para acelerar la primera notificacion y mantener retry durable. En escenarios de alta concurrencia, puede recibir el mismo payload 2 veces via HTTP, pero con **el mismo `X-Monetarie-Event-Id`** - es el mismo event, no un retry.

Para evitar impacto duplicado:

- **Dedupe por `X-Monetarie-Event-Id`** (recomendado - UUID unico por delivery, estable en retries y en la race condition arriba)
- O alternativamente dedupe por `end_to_end_id` + `event_type` cuando tenga sentido para el evento

Esto es comportamiento esperado, no error. Retries legitimos (despues de 5xx/timeout) tambien reusan el mismo `X-Monetarie-Event-Id`.

## External ID en los Webhooks

Cuando una transaccion fue creada con `external_id`, ese campo se incluye en el payload del webhook dentro del objeto `data`. Uselo para correlacionar el evento con el pedido en su sistema sin necesidad de hacer una consulta adicional.

## Requisitos del Endpoint

- La URL debe usar **HTTPS** (a menos que `allow_insecure: true` en el registro)
- Debe responder con estado `2xx` en hasta **30 segundos**
- El body de la respuesta es ignorado
- Recomendado responder rapido (`200 OK` inmediato) y procesar el evento de forma asincronica en su lado; delays largos reducen el throughput y aumentan chance de retries

## Proximos Pasos

- [Registrar Webhook](/es/webhooks-register) -- crear, listar y remover webhooks
- [Payloads de los Eventos](/es/webhooks-payloads) -- ejemplos de cada tipo de evento
