Webhooks -- Vision General
Los webhooks permiten que su aplicacion reciba notificaciones en tiempo real sobre eventos en la plataforma Owem Pay. Cuando ocurre un evento, Owem Pay envia un HTTP POST a la URL registrada.
Como Funciona
- Registre una URL de webhook en su cuenta
- Cuando ocurra un evento (ej: PIX recibido), Owem Pay envia un HTTP POST a su URL
- Su aplicacion procesa la notificacion y responde con estado
2xx(200, 201 o 204)
Eventos Disponibles
Owem Pay entrega solo eventos relacionados a PIX. Otros productos (boleto, cuentas, STA, transferencias no-PIX) 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 (verificado cada 5 min por worker) | Activo |
pix.charge.cancelled | cancelled | QR code cancelado antes del pago | Registrado, aun no disparado |
pix.payout.queued | queued | PIX enviado encolado por rate limit (ClientLimiter por merchant o bucket DICT BACEN). Retry automatico cada ~3s, TTL maximo de 2h | Activo |
pix.payout.processing | processing | PIX enviado, aguardando confirmacion BACEN | 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.return.received | settled | Devolucion PIX recibida (credito) | Activo |
pix.infraction.created | ACKNOWLEDGED | Infraccion PIX reportada por la contraparte via BACEN DICT; requiere defensa o genera bloqueo cautelar automatico (>R$1k) | Activo |
pix.infraction.resolved | CLOSED / CANCELLED | Infraccion resuelta (admin close, auto-deny o contraparte cancelo) | Activo |
pix.infraction.defense_submitted | defense_submitted | Defensa enviada por el merchant (portal o API); aguarda analisis BACEN | 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) |
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-Owem-Signature | Firma HMAC-SHA256 del payload (prefijo sha256=). En casos raros (webhook registrado sin secret), el valor literal es unsigned — vea nota abajo |
X-Owem-Timestamp | Unix timestamp en segundos del envio |
X-Owem-Event-Id | UUID unico de la delivery (para deduplicacion) |
X-Owem-Event-Type | Tipo del evento (ej: pix.charge.paid) |
Content-Type | Siempre application/json |
User-Agent | Siempre Owem-Webhook/1.0 — use para whitelisting en firewalls/WAF |
Signature unsigned cuando webhook no tiene secret
Si el webhook fue registrado sin campo secret (escenario legacy), el header X-Owem-Signature vale literalmente unsigned. Esto deshabilita la validacion HMAC de su lado. En la practica, POST /api/external/webhooks genera un secret aleatorio de 64 caracteres cuando el campo es omitido (desde session 80), por lo que el escenario solo aparece en registros muy antiguos o via admin bypass. Si recibe unsigned, registre un nuevo webhook con secret explicito y remueva el antiguo.
SHA256 en webhooks vs SHA512 en la API
La API usa HMAC-SHA512 para autenticar solicitudes que usted envia. Los webhooks enviados por Owem Pay usan HMAC-SHA256 en la firma X-Owem-Signature. Son algoritmos diferentes -- cada uno en su contexto.
Validando la Firma
Valide la firma para garantizar que la notificacion fue enviada por Owem Pay:
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)
);
}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 Owem 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.
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 Owem.
⚠️ Atencion — diferencia vs envio de solicitudes: En la firma HMAC-SHA512 de SOLICITUDES que usted envia, la ordenacion alfabetica de las claves ES obligatoria (el servidor Owem 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
Valide siempre
Nunca procese un webhook sin validar la firma. Esto protege contra solicitudes falsificadas.
Adicionalmente, valide que el X-Owem-Timestamp este dentro de ± 5 minutos de la hora actual (proteccion anti-replay) y deduplique eventos por X-Owem-Event-Id (proteccion contra retries).
Retry Policy
Si su URL retorna un estado diferente de 2xx (o timeout despues de 30 s), Owem Pay 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, eager via Task.start) | ~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 Owem proporcionando el X-Owem-Event-Id (o tener un operador con acceso admin replayar por el portal).
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 el worker Oban va a procesar el primer intento y la delivery ya tiene mas de 5 minutos desde su creacion (inserted_at), el envio es abortado y el estado va directo a expired. Esto impide que reprocesamientos tardios (por acumulacion de cola, pod reiniciado, etc.) disparen notificaciones de eventos ya viejos. Replays manuales solicitados al soporte Owem pasan por el flag interno manual_replay: true y bypass esa guarda — el cliente recibe la notificacion normalmente.
Durabilidad
Antes de intentar la primera entrega, el evento es persistido en webhook_deliveries en PostgreSQL. Si el pod que dispara el webhook cae durante la entrega, Oban retoma automaticamente en el proximo retry — ningun evento es perdido.
Idempotencia
Su aplicacion debe ser idempotente: si recibe el mismo evento mas de una vez (identificado por el X-Owem-Event-Id), debe procesarlo sin duplicar efectos.
Replay manual via admin
Si una delivery fallo y usted necesita re-enviar, el equipo Owem puede ejecutar replay manual via admin dashboard. Contacte al soporte con el event_id de la delivery.
Entregas duplicadas (race condition conocida)
El sistema usa un mecanismo de "eager delivery" para acelerar la primera entrega + un Oban worker como fallback durable de retry. En escenarios de alta concurrencia, esos dos caminos pueden disparar el mismo webhook en paralelo (ventana de ~1 segundo). En esa situacion usted recibe el mismo payload 2 veces via HTTP, pero con el mismo X-Owem-Event-Id — es el mismo event, no un retry.
Para evitar impacto duplicado:
- Dedupe por
X-Owem-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_typecuando tenga sentido para el evento
Esto es comportamiento esperado, no error. Retries legitimos (despues de 5xx/timeout) tambien reusan el mismo X-Owem-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: trueen el registro) - Debe responder con estado
2xxen hasta 30 segundos (receive_timeoutconfigurado en el worker de entrega) - El body de la respuesta es ignorado
- Recomendado responder rapido (
200 OKinmediato) y procesar el evento de forma asincronica en su lado; delays largos reducen el throughput y aumentan chance de retries
Proximos Pasos
- Registrar Webhook -- crear, listar y remover webhooks
- Payloads de los Eventos -- ejemplos de cada tipo de evento