Webhooks
Modelos de webhook: V1 (postbackUrl) e V2 (global)
A Pagnovo oferece dois modelos de webhook que rodam em paralelo — não são mutuamente exclusivos. Você pode usar um, outro, ou os dois ao mesmo tempo:
| Modelo | Como configura | Quando dispara | Distinção no payload |
|---|---|---|---|
V1 — postbackUrl per-transação | Passa o campo postbackUrl no body de cada POST /transactions/v2/purchase ou POST /withdraws/cash-out | Status muda nessa transação específica (uma URL por tx) | Payload camelCase plano, sem campo event |
| V2 — Webhook global por User | Cadastra uma URL única em Webhooks > Endpoints (rota /auth/webhooks) com a lista de eventos que você quer receber | TODOS os eventos do tipo subscrito disparam, independente da transação | Payload tem campos event + environment no topo |
⚠️ Coexistência: se você tem AMBOS configurados (passou
postbackUrlnuma transação + tem um webhook global V2 ativo subscrito ao evento correspondente), seu endpoint recebe os dois POSTs — um pra cada modelo. Sua URL pode ser a mesma; basta seu handler distinguir pelo formato do payload.
✅ O formato é definido pelo canal, não pela conta. Todo webhook cadastrado em Webhooks > Endpoints (CRUD) é entregue sempre no formato V2 — envelope
{ event, environment, payload }. O formato V1 (camelCase plano) é exclusivo dopostbackUrllegado. Ou seja: se você usa só o CRUD (sempostbackUrl), sempre recebe V2.
Recomendamos V2 pra novas integrações: um único setup cobre todas as transações automaticamente, tem rastreio de origem (source_type, invoice_id, etc), e suporta eventos extras (subscriptions, infrações) que V1 não cobre.
Quando escolher v1
- Você já tem uma integração v1 funcionando em produção. Não há urgência para migrar; v1 é suportada indefinidamente.
- Você tem dependência rígida do formato camelCase plano por causa de outro sistema que consome seus webhooks downstream.
- Você só precisa de atualizações básicas de status de transação e não se importa com ISPB, separação de pagador/recebedor ou eventos de infração.
Se nada disso se aplica, escolha v2.
Quando escolher v2
- Você está começando uma integração nova hoje.
- Você precisa de informações de ISPB / instituição para conciliação contra extratos bancários.
- Você quer roteamento por tipo de evento no seu handler (
switch (event) { case "cashin.paid": ... }) em vez de inferir a partir de combinações detypeestatus. - Você quer webhooks de infração entregues como seu próprio tipo de evento em vez de embutidos dentro do payload da transação.
Escolha v2 por padrão para qualquer coisa nova.
Eventos disponíveis (V2)
Em V2 (Webhook global), você seleciona em Configurações > Webhooks a lista exata de eventos que cada URL deve receber. Os 12 eventos abaixo são todos os que o sistema dispara hoje:
Eventos de cash-in (transação):
cashin.paid— Transação aprovada (PIX confirmado ou cartão capturado).cashin.refunded— Estorno concluído.
Eventos de cash-out (saque):
cashout.success— Saque concluído (SPI confirmou).cashout.returned— Saque devolvido pelo banco do recebedor (chave inválida, etc).cashout.failed— Saque falhou tecnicamente.
Disputas / Bloqueios:
infraction.updated— Chargeback (disputa de cartão), bloqueio cautelar de compliance, ou MED criado/atualizado. Use o payload pra distinguir o tipo.
Subscription lifecycle (assinatura recorrente):
subscription.created— Nova subscription criada.subscription.activated— Status virou ACTIVE (criação direta ou retomada de PAUSED).subscription.paused— Status virou PAUSED.subscription.canceled— Cancelamento manual (cliente ou sistema).subscription.past_due— Cliente atrasou pagamento (invoice OVERDUE).subscription.expired— AtingiumaxCyclesouexpiresAt.
Em V1 (postbackUrl per-tx), os toggles legacy continuam controlando: sendTransactionPaidWebhook, sendTransactionRefundedWebhook, sendTransactionChargebackWebhook, sendTransactionDisputeWebhook. Esses toggles só afetam V1 — V2 usa a array events do model Webhook em vez de flags booleanas.
Desabilite o que você não processa. Cada webhook que você recebe mas ignora é um para o qual seu endpoint ainda precisa responder
200— é uma carga pequena mas desnecessária.
Distinguindo V1 e V2 no mesmo endpoint
Se você manda V1 e V2 pra mesma URL, distinguir no handler é trivial — V2 tem o envelope { event, environment, ... }; V1 não:
function handleWebhook(body: unknown) {
if (typeof body === "object" && body !== null && "event" in body) {
// V2: { event: "cashin.paid", environment: "TEST", payload: { ... } }
return handleV2(body as V2Webhook);
}
// V1: { type: "TRANSACTION", status: "APPROVED", ...campos planos }
return handleV1(body as V1Webhook);
}
Idempotência: se você se inscreveu em V2 cashin.paid E também passou postbackUrl em uma transação, vai receber 2 POSTs pro mesmo evento. Garanta que seu handler é idempotente — armazene o transactionId (V1) ou payload.transaction_id (V2) e dedup.
Migrando v1 → v2
Uma receita segura:
Passo 1 — Escreva o handler v2 em um controller dual-mode.
Detecte a versão pela estrutura: v2 sempre tem campos event e payload no topo; v1 tem type e campos planos.
function handleWebhook(body: unknown) {
if (typeof body === "object" && body !== null && "event" in body && "payload" in body) {
return handleV2(body as V2Webhook);
}
return handleV1(body as V1Webhook);
}
Passo 2 — Deploya o handler dual em produção, ainda configurado como v1.
Nada muda operacionalmente. Seu caminho v1 continua rodando como antes; o caminho v2 é código dormente, nunca executado.
Passo 3 — Teste v2 contra um ambiente de staging.
Use uma conta de staging configurada para webhooks v2 apontando para uma URL de staging. Rode sua suite de testes, verifique equivalência campo-a-campo com o que seu handler v1 gravava antes.
Passo 4 — Vire a flag de versão no dashboard.
O próximo evento chega em v2. Seu handler dual roteia corretamente. Zero downtime.
Passo 5 — Remova o caminho v1 depois de um período de estabilização.
Depois de 30 dias sem incidentes, delete o código legado de v1. Até lá, mantenha — se aparecer uma regressão, você pode voltar a flag no dashboard imediatamente.
A migração inteira costuma ser menos de um dia de trabalho de engenharia se seus testes de webhook estiverem decentes. O grosso é o mapeamento de campos camelCase → snake_case.