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 → API e Desenvolvedores → Webhooks. Guarde numa variável de ambiente do seu backend. Se vazar, regenere pelo painel. O antigo para de funcionar na hora.

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.