Skip to main content
Todo webhook chega assinado com HMAC-SHA256 no header X-Webhook-Signature. Validar essa assinatura é o que separa “recebi uma notificação” de “recebi uma notificação nossa”.
A sua callbackUrl é pública. Sem validação, um curl com {"status":"paid"} é suficiente para alguém levar o seu produto sem pagar.

Onde fica o secret

Cada loja tem o seu, em Dashboard → Equipe & API → Developer API. Guarde numa variável de ambiente do seu backend. Se vazar, regenere pelo painel. O antigo para de funcionar na hora.

Um secret por URL

Na mesma tela, cada URL da conta pode ter o secret dela. Deixe vazio e ela é assinada com o secret da conta, que é como toda URL funciona por padrão: quem já tinha URLs configuradas não precisa mexer em nada. Vale a pena separar quando as URLs vão para sistemas diferentes. Um secret por destino significa que o que vazar no sistema de BI não assina evento para o backend de produção.
O callbackUrl que você manda ao criar uma cobrança não está na lista da conta, então ele é sempre assinado com o secret da conta.

Como calcular

O HMAC é sobre o corpo cru da requisição, a string exata que chegou, antes de qualquer JSON.parse. Se você parsear e re-serializar, a ordem das chaves ou o espaçamento mudam e a assinatura nunca vai bater. Compare com timingSafeEqual ou equivalente. Comparação com === vaza informação pelo tempo de execução.

Idempotência

O header X-Webhook-Id vem no formato evento:id, como payment.paid:psa_abc123. Reentrega do mesmo evento usa o mesmo valor.
Deduplique antes de creditar saldo, enviar licença ou disparar e-mail. Depois de entregar não tem como voltar atrás, e a reentrega não é hipótese remota: é o comportamento normal quando o seu servidor demora mais de 5 segundos para responder.

Erros comuns

Quase sempre é middleware de JSON rodando antes do handler e consumindo o corpo. Em Express, express.json() global quebra a validação. Registre a rota do webhook com express.raw() antes do parser global.
Proxy ou CDN reescrevendo o corpo. Verifique se não há compressão, reformatação de JSON ou WAF alterando a requisição no caminho.
Você está comparando a assinatura contra um corpo re-serializado. Guarde os bytes originais em vez de reconstruir o JSON.