Skip to Content
WebhooksVisão geral

Webhooks — Visão geral

Webhooks são o sinal oficial. Callbacks do navegador (onSuccess) e visões do dashboard são conveniência; webhooks são a verdade.

Garantias de entrega

  • Pelo menos uma vez. Um único evento pode ser entregue até 6 vezes se o seu servidor não retornar 2xx dentro do timeout. Torne o seu handler idempotente — deduplique em X-Delivery (o payload não tem campo event_id; o UUID estável da entrega é a chave de idempotência).
  • Um evento por requisição HTTP. Sem batching.
  • Isolamento por endpoint. Se você tem múltiplos endpoints registrados, cada um tem a própria trilha de entrega + retry. Uma URL de lojista lenta não consegue inanir as outras — cada host tem o próprio circuit breaker.
  • Assinado. Todo payload leva um header X-Signature (e durante a janela de 24 horas depois de uma rotação, também um X-Signature-Prev). Verifique antes de fazer qualquer coisa com o body. Veja Verificação de assinatura.

Tipos de evento que se pode assinar

EventoDispara quando…
payment.settledA transferência on-chain atingiu a contagem de confirmações da rede. Use isto para marcar pedidos como pagos.
payment.failedUm pagamento fiat foi explicitamente rejeitado pelo provedor (atualmente: webhook do Stripe sinalizando falha). Não dispara para timeouts de cripto — esses afloram como checkout.expired, e pagamentos cripto curtos afloram como payment.underpaid.
payment.underpaidFundos chegaram, mas abaixo do total do pedido (típico: taxa de transferência de stablecoin descontada do valor).
payment.overpaidFundos chegaram em excesso ao total do pedido. O excedente é registrado mas não é auto-reembolsado.
order.createdUm novo pedido foi aberto — pela sua chamada de API B2B ou por uma conversão de checkout-session.
order.canceledUm pedido foi movido para cancelado. O data.reason do payload distingue cancel manual de payment_timeout (pedido não pago e velho varrido pelo worker).
order.resolvedUm pedido PARTIAL_PAID foi resolvido para PAID — o lojista aceitou o déficit.
order.reopenedUm pedido previamente auto-cancelado (canceled_reason=payment_timeout) foi reaberto pelo lojista.
checkout.createdUm comprador abriu o checkout para um pedido.
checkout.completedO fluxo do lado do comprador terminou (não implica liquidação on-chain — use payment.settled para isso).
checkout.expiredO comprador abandonou e o TTL da sessão acabou.
payment.refund.requestedUm registro de reembolso foi criado — seja a partir de uma chamada de API iniciada pelo lojista, seja a partir de um formulário de pedido de reembolso submetido pelo cliente.
payment.refund.approvedUm reembolso pendente passou pelo seu workflow de aprovação.
payment.refund.rejectedUm reembolso pendente foi negado.
payment.refund.executedA transferência on-chain do reembolso foi confirmada e o registro foi para o estado terminal executed.
refund_request.createdUm token de pedido de reembolso foi criado. data.source é b2b / dashboard / renewal. Opcional inscrever-se — útil para pipelines de auditoria que acompanham qual token está ativo no momento por pedido.
refund_request.renewal_requestedUm comprador clicou em “Pedir link novo” depois que o token expirou. Fortemente recomendado inscrever-se — é o sinal de que o widget de renovação tem um novo item para agir.
refund_request.renewedUma renovação foi aprovada e um novo token substituiu o antigo. data.old_token / data.new_token formam a cadeia de auditoria.
refund_request.canceledUm lojista moveu um token para CANCELED pelo dashboard (por exemplo, negou um pedido de renovação, matou um link vivo). Idempotente — só a primeira transição emite. data.reason é a nota opcional do lojista.

O dashboard busca esta lista em GET /v1/webhooks/event-types para que o formulário de criação / edição de endpoint sempre bata com o que a plataforma efetivamente emite. Tentar se inscrever em um evento que a gente não dispara é rejeitado na criação com um erro claro.

Eventos de teste não são assináveis. O botão Send Test por endpoint no dashboard faz POST de um envelope webhook.test.ping síncrono para aquele único endpoint (contornando a pipeline de retry), e o caminho legado de “Send test event” no nível do lojista distribui um envelope webhook.test para todo endpoint ativo independentemente do filtro. Nenhum dos dois aparece no catálogo acima — você recebe pelo fato de ter um endpoint registrado, não por inscrever-se.

Inscreva-se só nos eventos que você trata. Cada endpoint tem o próprio filtro de evento; o wildcard "*" significa “todo evento, incluindo os adicionados no futuro”. Inscrever-se em menos eventos mantém o seu handler mais limpo e reduz a área de superfície que a gente tem que retentar em erros.

Payload + headers

O corpo HTTP é o objeto de dados específico do evento, direto. Sem envelope externo estilo Stripe — campos como tipo do evento, ID da entrega e timestamp de emissão ficam nos headers. Para payment.settled o body fica assim:

{ "receipt_id": "rcp_…", "order_id": "ord_…", "payment_intent_id": "pin_…", "checkout_session_id": "cst_…", "merchant_id": "mer_…", "customer_id": "cus_…", "total": "49.00", "currency": "USD", "payment_method": "crypto", "token": "USDC", "network": "polygon", "tx_hash": "0x…", "deposit_address": "0x…", "treasury_address": "0x…", "amount_received": "49.00", "confirmations": 5, "metadata": { /* por evento */ } }

Outros eventos carregam o próprio conjunto de campos — veja as structs de publisher em payment-service/internal/domain/events.go para o formato canônico até a documentação por evento chegar. Nomes de campo são estáveis (lower snake_case); o tx hash on-chain é sempre tx_hash (não transaction_hash).

Headers na requisição de entrada

Content-Type: application/json X-Event: payment.settled X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7 Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7 X-Timestamp: 1729536000 X-Signature: sha256=9a8b7c… X-Signature-Prev: sha256=fa31b2… (só durante uma janela de tolerância de rotação)
HeaderO que é
X-EventO tipo do evento (por exemplo, payment.settled). Rotear nisso na camada de proxy se você quer pular o parse do JSON.
X-DeliveryUUID que identifica a linha de entrega. Estável em todas as retentativas do mesmo par (event, endpoint) — use como sua chave de idempotência.
Idempotency-KeyEspelha o X-Delivery (mesmo valor). Definido em toda entrega — segue a convenção do Stripe / GitHub.
X-TimestampUnix-segundos do momento em que a tentativa foi enviada. Assinado dentro do payload para que um par (body, X-Signature) capturado não possa ser replayado indefinidamente — rejeite entregas cujo timestamp esteja fora da sua janela de tolerância.
X-Signaturesha256=<hex> de HMAC-SHA256(secret, X-Timestamp + "." + raw_body). Veja Verificação de assinatura.
X-Signature-PrevMesmo algoritmo com o segredo anterior. Presente só na janela de 24 horas depois que você rotaciona — deixa verificadores rodando qualquer das duas chaves continuarem aceitando entregas durante o cutover. Depois da janela fechar o header para de ser enviado.

Cronograma de retry

Se o seu endpoint não retornar 2xx dentro do timeout, a gente retenta neste cronograma (timestamps relativos à primeira tentativa):

TentativaAtrasoAcumulado
10s0s
2+1 min1m
3+5 min6m
4+15 min21m
5+1 hora1h 21m
6+6 horas7h 21m

Depois da tentativa 6 falhar, a entrega é movida para dead letter e o e-mail da conta do lojista é notificado. Eventos com dead-letter podem ser replayados pelo painel Developers → Webhooks → Delivery history do dashboard, ou diretamente via POST /v1/webhooks/deliveries/:id/replay. Cada replay cria uma nova linha de entrega com o próprio X-Delivery — a cadeia de auditoria liga de volta ao original via parent_delivery_id para que retentativas de replays não escondam o evento de origem.

Registrar um endpoint

No dashboard do lojista :

  1. Developers → Webhooks+ Add endpoint
  2. Cole a sua URL — só https://… (HTTP puro é rejeitado; o formulário de criação também bloqueia localhost, ranges de IP privados e URLs com userinfo)
  3. Escolha os eventos para assinar (ou * para todos)
  4. Escolha o ambiente — test ou live (cada um tem o próprio segredo; eles nunca se cruzam)
  5. Salve → o dashboard mostra o segredo de assinatura (whsec_…) uma vez. Guarde do lado do servidor; você vai precisar para as próximas duas features.

Você pode registrar até 10 endpoints por ambiente por lojista (por exemplo, um para entrega em produção, um para espelhamento em staging, um para um notificador no Slack). Cada um mantém o próprio estado de retry, segredo e circuit breaker por host.

Ações de ciclo de vida em cada endpoint

O menu ⋮ em cada card de endpoint expõe:

  • Edit — muda a URL, descrição ou lista de inscrições. A URL nova é re-validada com as mesmas regras https:///SSRF da criação.
  • Send Test — faz POST síncrono de um envelope webhook.test.ping assinado com o seu segredo atual. O dashboard mostra o status HTTP, latência e um snippet de 512 bytes da sua resposta. Contorna a pipeline RMQ, então a resposta é imediata.
  • Rotate Secret — gera um segredo novo. O anterior permanece válido por 24 horas (entregas levam tanto X-Signature quanto X-Signature-Prev durante a janela para que verificadores rodando qualquer das duas chaves continuem aceitando eventos enquanto você redeploya).
  • Reveal Secret — re-exibe o segredo existente. Condicionado a verificação 2FA recente e registrado no log de auditoria; use só quando você perdeu a sua cópia e o Rotate não é aceitável.
  • Enable / Disable — alterna is_active sem perder o histórico de entrega. Endpoints desabilitados ficam no dashboard mas não recebem novas entregas.
  • Delete — permanente. Use Disable se você pode reabilitar depois.

Dicas para handlers

  1. Retorne 2xx rápido. Confirme com 200 OK antes de fazer trabalho pesado — empurre a entrega para uma fila em background. O timeout por tentativa é de 10 segundos; segurar a resposta mais do que isso dispara um retry. O timeout é do lado da plataforma e não é configurável pelo lojista — contate o suporte se o seu handler genuinamente precisa de mais tempo.
  2. Deduplique em X-Delivery (ou Idempotency-Key — mesmo valor). Mesmo que você retorne 2xx, um proxy upstream pode derrubar a conexão e disparar um retry; o ID da entrega é estável em toda retentativa da mesma linha de entrega, então é a chave certa.
  3. Tolere tipos de evento desconhecidos. Eventos novos podem aparecer; retorne 200 e no-op no lugar de 4xx, ou você vai encher a fila de retry.
  4. Logue X-Delivery junto com a sua lógica de negócio. Quando algo dá errado, essa é a chave de junção entre o nosso lado e o seu.

Próximos passos