X-Webhook-Signature. Validar essa
assinatura é o que separa “recebi uma notificação” de “recebi uma notificação nossa”.
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 qualquerJSON.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 headerX-Webhook-Id vem no formato evento:id, como payment.paid:psa_abc123.
Reentrega do mesmo evento usa o mesmo valor.
Erros comuns
A assinatura nunca bate
A assinatura nunca bate
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.Funciona local e falha em produção
Funciona local e falha em produção
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.
Bate às vezes
Bate às vezes
Você está comparando a assinatura contra um corpo re-serializado. Guarde os bytes
originais em vez de reconstruir o JSON.

