Webhooks -- Visão Geral
Webhooks permitem que sua aplicação receba notificações em tempo real sobre eventos na plataforma Owem Pay. Quando um evento ocorre, a Owem Pay envia um HTTP POST para a URL cadastrada.
Como Funciona
- Cadastre uma URL de webhook na sua conta
- Quando um evento ocorrer (ex: PIX recebido), a Owem Pay envia um HTTP POST para sua URL
- Sua aplicação processa a notificação e responde com status
2xx(200, 201 ou 204)
Eventos Disponíveis
A Owem Pay entrega apenas eventos relacionados ao PIX. Outros produtos (boleto, contas, STA, transferências não-PIX) não estão em escopo. Qualquer tentativa de assinar eventos fora da tabela abaixo é rejeitada com events: contains invalid events: ....
| Evento | Status body | Descrição | Disparo |
|---|---|---|---|
pix.charge.created | created | QR code gerado ou cash-in iniciado | Ativo |
pix.charge.paid | paid | PIX recebido e liquidado | Ativo |
pix.charge.expired | expired | QR code expirou sem pagamento (verificado a cada 5 min por worker) | Ativo |
pix.charge.cancelled | cancelled | QR code cancelado antes de pagamento | Registrado, ainda não disparado |
pix.payout.queued | queued | PIX enviado enfileirado por rate limit (ClientLimiter por merchant ou bucket DICT BACEN). Retry automático a cada ~3s, com TTL máximo de 2h | Ativo |
pix.payout.processing | processing | PIX enviado, aguardando confirmação BACEN | Ativo |
pix.payout.confirmed | settled | PIX enviado e confirmado (terminal) | Ativo |
pix.payout.failed | rejected | PIX enviado rejeitado pelo SPI (terminal) | Ativo |
pix.payout.returned | returned | PIX enviado devolvido | Ativo |
pix.refund.requested | requested | Pedido de devolução recebido (infração BACEN); bloqueio cautelar criado no saldo do cliente | Ativo |
pix.refund.completed | settled / completed | Análise da defesa finalizada e devolução executada (ou liberada) | Ativo |
pix.return.received | settled | Devolução PIX recebida (crédito) | Ativo |
pix.infraction.created | ACKNOWLEDGED | Infração PIX reportada pela contraparte via BACEN DICT; requer defesa ou gera bloqueio cautelar automático (>R$1k) | Ativo |
pix.infraction.resolved | CLOSED / CANCELLED | Infração resolvida (admin close, auto-deny ou contraparte cancelou) | Ativo |
pix.infraction.defense_submitted | defense_submitted | Defesa submetida pelo merchant (portal ou API); aguarda análise BACEN | Ativo |
webhook.test | test | Teste manual. Disponível apenas via Admin/Merchant portal — a External API não expõe endpoint para disparar teste | Disparo manual (não External API) |
pix.charge.cancelled ainda não é disparado
O evento está no enum e pode ser assinado, mas o sistema não possui fluxo de cancelamento de QR code hoje. Se você assinar, o POST /webhooks responde 201 normalmente — porém nenhuma notificação chegará. Continue monitorando pix.charge.expired para o ciclo natural de vida do QR.
Segurança
Cada notificação inclui headers de segurança e identificação para validação:
| Header | Descrição |
|---|---|
X-Owem-Signature | Assinatura HMAC-SHA256 do payload (prefixo sha256=). Em casos raros (webhook cadastrado sem secret), o valor literal é unsigned — veja nota abaixo |
X-Owem-Timestamp | Unix timestamp em segundos do envio |
X-Owem-Event-Id | UUID único da delivery (para deduplicação) |
X-Owem-Event-Type | Tipo do evento (ex: pix.charge.paid) |
Content-Type | Sempre application/json |
User-Agent | Sempre Owem-Webhook/1.0 — use para whitelisting em firewalls/WAF. Evolução futura seguirá o padrão Owem-Webhook/{version}; filtre por prefixo Owem-Webhook/ se quiser ficar imune a novas versões |
Signature unsigned quando webhook não tem secret
Se o webhook foi cadastrado sem campo secret (cenário legado), o header X-Owem-Signature vale literalmente unsigned. Isso desabilita a validação HMAC do seu lado. Na prática, POST /api/external/webhooks gera um secret aleatório de 64 caracteres quando o campo é omitido (desde session 80), então o cenário só aparece em registros muito antigos ou via admin bypass. Se você receber unsigned, cadastre um novo webhook com secret explícito e remova o antigo.
SHA256 nos webhooks vs SHA512 na API
A API usa HMAC-SHA512 para autenticar requisições que você envia. Os webhooks enviados pela Owem Pay usam HMAC-SHA256 na assinatura X-Owem-Signature. São algoritmos diferentes -- cada um no seu contexto.
Validando a Assinatura
Valide a assinatura para garantir que a notificação foi enviada pela 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 o body RAW, não re-serializado
Você deve usar o corpo exato da requisição HTTP como os bytes chegaram na sua aplicação. Se fizer JSON.parse e depois JSON.stringify, os bytes resultantes não serão idênticos ao que a Owem usou para assinar, e a validação falhará.
Em Express/Node: use express.raw({ type: 'application/json' }) ou guarde o body antes de qualquer middleware de parse.
Em outras frameworks: configure para capturar o raw body antes do middleware JSON.
Ordenação de chaves em WEBHOOKS: NÃO é necessária
Para validação de webhooks (HMAC-SHA256) você NÃO precisa ordenar as chaves — use o body raw como recebido no HTTP request do Owem.
⚠️ Atenção — diferença vs envio de requisições: Na assinatura HMAC-SHA512 de REQUISIÇÕES que você envia, a ordenação alfabética das chaves É obrigatória (o servidor Owem reordena antes de validar). Não confunda os dois cenários:
- Webhook recebido (HMAC-SHA256): valide o body raw sem reordenar
- Request enviada (HMAC-SHA512): ordene suas chaves alfabeticamente antes de assinar
Valide sempre
Nunca processe um webhook sem validar a assinatura. Isso protege contra requisições falsificadas.
Adicionalmente, valide que o X-Owem-Timestamp está dentro de ± 5 minutos da hora atual (proteção anti-replay — o servidor não rejeita webhooks "antigos" por padrão; essa verificação cabe ao seu endpoint como defense-in-depth) e deduplique eventos por X-Owem-Event-Id (proteção contra retries).
Retry Policy
Se sua URL retornar um status diferente de 2xx (ou timeout após 30 s), a Owem Pay realiza até 8 tentativas com backoff exponencial. O total de tempo entre a primeira e a oitava tentativa é aproximadamente 7h45min:
| Tentativa | Delay desde tentativa anterior | Tempo acumulado |
|---|---|---|
| 1a | — (imediato, eager via Task.start) | ~50–200 ms |
| 2a | 30 segundos | ~30 s |
| 3a | 2 minutos | ~2,5 min |
| 4a | 10 minutos | ~12,5 min |
| 5a | 30 minutos | ~42,5 min |
| 6a | 1 hora | ~1,75 h |
| 7a | 2 horas | ~3,75 h |
| 8a | 4 horas | ~7,75 h |
Após 8 tentativas sem sucesso, o webhook_delivery é marcado com status failed e não é reenviado automaticamente. Você pode solicitar replay manual ao suporte Owem fornecendo o X-Owem-Event-Id (ou ter um operador com acesso admin replayar pelo portal).
Status de uma delivery
Cada delivery passa pelos status: pending (criada, aguardando entrega) → delivered (2xx recebido) OU failed (8 tentativas exauridas) OU expired (replay protection).
Sobre expired: quando o worker Oban vai processar a primeira tentativa e a delivery já tem mais de 5 minutos desde sua criação (inserted_at), o envio é abortado e o status vai direto para expired. Isso impede que reprocessamentos tardios (por acúmulo de fila, pod reiniciado, etc.) disparem notificações de eventos já velhos. Replays manuais solicitados ao suporte Owem passam pelo flag interno manual_replay: true e bypass esse guard — o cliente recebe a notificação normalmente.
Durabilidade
Antes de tentar a primeira entrega, o evento é persistido em webhook_deliveries no PostgreSQL. Se o pod que dispara o webhook cair durante a entrega, o Oban retoma automaticamente no próximo retry — nenhum evento é perdido.
Idempotência
Sua aplicação deve ser idempotente: se receber o mesmo evento mais de uma vez (identificado pelo X-Owem-Event-Id), deve processá-lo sem duplicar efeitos.
Replay manual via admin
Se uma delivery falhou e você precisa re-enviar, o time Owem pode executar replay manual via admin dashboard. Contate o suporte com o event_id da delivery.
Entregas duplicadas (race condition conhecida)
O sistema usa um mecanismo de "eager delivery" para acelerar a primeira entrega + um Oban worker como fallback durável de retry. Em cenários de alta concorrência, esses dois caminhos podem disparar o mesmo webhook em paralelo (janela de ~1 segundo). Nessa situação você recebe o mesmo payload 2 vezes via HTTP, mas com o mesmo X-Owem-Event-Id — é o mesmo event, não um retry.
Para evitar impacto duplicado:
- Dedupe por
X-Owem-Event-Id(recomendado — UUID único por delivery, estável em retries e na race condition acima) - Ou alternativamente dedupe por
end_to_end_id+event_typequando fizer sentido para o evento
Isso é comportamento esperado, não erro. Retries legítimos (após 5xx/timeout) também reusam o mesmo X-Owem-Event-Id.
External ID nos Webhooks
Quando uma transação foi criada com external_id, esse campo é incluído no payload do webhook dentro do objeto data. Use-o para correlacionar o evento com o pedido no seu sistema sem precisar fazer uma consulta adicional.
Requisitos do Endpoint
- A URL deve usar HTTPS (a menos que
allow_insecure: trueno cadastro) - Deve responder com status
2xxem até 30 segundos (receive_timeoutconfigurado no worker de entrega) - O body da resposta é ignorado
- Recomendado responder rápido (
200 OKimediato) e processar o evento de forma assíncrona no seu lado; delays longos reduzem o throughput e aumentam chance de retries
Próximos Passos
- Cadastrar Webhook -- criar, listar e remover webhooks
- Payloads dos Eventos -- exemplos de cada tipo de evento