Skip to content

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

  1. Registre una URL de webhook en su cuenta
  2. Cuando ocurra un evento (ej: PIX recibido), Owem Pay envia un HTTP POST a su URL
  3. 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: ....

EventoStatus bodyDescripcionDisparo
pix.charge.createdcreatedQR code generado o cash-in iniciadoActivo
pix.charge.paidpaidPIX recibido y liquidadoActivo
pix.charge.expiredexpiredQR code expiro sin pago (verificado cada 5 min por worker)Activo
pix.charge.cancelledcancelledQR code cancelado antes del pagoRegistrado, aun no disparado
pix.payout.queuedqueuedPIX enviado encolado por rate limit (ClientLimiter por merchant o bucket DICT BACEN). Retry automatico cada ~3s, TTL maximo de 2hActivo
pix.payout.processingprocessingPIX enviado, aguardando confirmacion BACENActivo
pix.payout.confirmedsettledPIX enviado y confirmado (terminal)Activo
pix.payout.failedrejectedPIX enviado rechazado por el SPI (terminal)Activo
pix.payout.returnedreturnedPIX enviado devueltoActivo
pix.refund.requestedrequestedSolicitud de devolucion recibida (infraccion BACEN); bloqueo cautelar creado en el saldo del clienteActivo
pix.refund.completedsettled / completedAnalisis de la defensa finalizado y devolucion ejecutada (o liberada)Activo
pix.return.receivedsettledDevolucion PIX recibida (credito)Activo
pix.infraction.createdACKNOWLEDGEDInfraccion PIX reportada por la contraparte via BACEN DICT; requiere defensa o genera bloqueo cautelar automatico (>R$1k)Activo
pix.infraction.resolvedCLOSED / CANCELLEDInfraccion resuelta (admin close, auto-deny o contraparte cancelo)Activo
pix.infraction.defense_submitteddefense_submittedDefensa enviada por el merchant (portal o API); aguarda analisis BACENActivo
webhook.testtestPrueba manual. Disponible solo via Admin/Merchant portal — la External API no expone endpoint para disparar pruebaDisparo 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:

HeaderDescripcion
X-Owem-SignatureFirma HMAC-SHA256 del payload (prefijo sha256=). En casos raros (webhook registrado sin secret), el valor literal es unsigned — vea nota abajo
X-Owem-TimestampUnix timestamp en segundos del envio
X-Owem-Event-IdUUID unico de la delivery (para deduplicacion)
X-Owem-Event-TypeTipo del evento (ej: pix.charge.paid)
Content-TypeSiempre application/json
User-AgentSiempre 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:

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)
  );
}

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:

IntentoDelay desde intento anteriorTiempo acumulado
1o— (inmediato, eager via Task.start)~50–200 ms
2o30 segundos~30 s
3o2 minutos~2,5 min
4o10 minutos~12,5 min
5o30 minutos~42,5 min
6o1 hora~1,75 h
7o2 horas~3,75 h
8o4 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_type cuando 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: true en el registro)
  • Debe responder con estado 2xx en hasta 30 segundos (receive_timeout configurado en el worker de entrega)
  • 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

Owem Pay Instituição de Pagamento — ISPB 37839059