Stripe diz No signatures found matching the expected signature for payload. GitHub diz We could not verify your signature. Shopify diz HMAC validation failed. Tres erros diferentes, mas a causa raiz geralmente e uma de sete coisas, e 90% das vezes e a primeira.
Este artigo ordena as sete causas por frequencia, com um fix copia-cola e um comando de debug local de uma linha para cada.
Resumo em 30 segundos
- A assinatura do webhook = HMAC-SHA256(secret, raw_body). A formula nunca muda; o que muda e o formato do header e os detalhes de codificacao.
- As 7 causas por frequencia: body parseado em vez de raw / secret incorreto / encoding incorreto (hex vs base64) / tolerancia de timestamp / secret commitado no codigo / usar === para comparar / secret vazado em logs
- Cada provedor usa formato e encoding diferentes: GitHub usa sha256= mais hex, Shopify usa base64 puro, Slack usa v0= mais hex, Stripe usa t=…,v1= mais hex
- Use o Validador de Assinatura de Webhook da Piick no seu navegador para reproduzir qualquer assinatura localmente e ver se bate, todos os 5 provedores suportados
As 7 causas, da mais comum a mais sutil
Causa 1: Voce verifica o body parseado como JSON, nao o raw body
Esta e a causa raiz de 50% das falhas de assinatura. Todos os provedores assinam raw bytes, mas voce esta assinando JSON.stringify(req.body). Express, Flask e Next.js fazem parse do body por padrao e depois re-stringificam, a ordem das chaves ou os espacos mudam, e a assinatura nao bate mais.
O fix:
- Express: use express.raw() em vez de express.json(), no handler passe req.body (Buffer) direto para o verificador
- Flask: use request.get_data(as_text=False), nao request.json
- Next.js API route: adicione export const config com api bodyParser false
Debug: imprima os primeiros 30 caracteres de req.body e Content-Length, depois compare com os dados POST originais.
Causa 2: Secret incorreto
Em local voce usa o secret efemero do Stripe CLI (impresso pelo stripe listen). Em producao voce usa o do dashboard. O mesmo endpoint tem strings de secret diferentes em dois ambientes.
O fix: configure cada endpoint em cada ambiente de forma independente. Leia env vars de um secret manager. Nunca faca hard-code no codigo.
Debug: verifique o mesmo valor de secret em tres lugares — dashboard, Stripe CLI, env do codigo — e confirme que batem exatamente.
Causa 3: Encoding incorreto
90% dos provedores usam hex, mas Shopify e o unico que usa base64. Se voce envia hex para Shopify, a assinatura nunca vai bater.
O fix: siga o spec do provedor a risca, nao alterne hex / base64 manualmente. Use o dropdown de provedor do validador de webhook da Piick.
Debug: compare o comprimento do hex gerado (64 caracteres) contra o comprimento do base64 (44 caracteres). Comprimento certo geralmente significa formato certo.
Causa 4: Tolerancia de timestamp nao configurada ou mal configurada (so Stripe)
O Stripe enviou o webhook ha 5 minutos, mas seu servidor ainda rejeita. O Stripe inclui um timestamp em t=…, e o SDK so aceita requests dentro de ±5 minutos por padrao.
O fix: Stripe.webhooks.constructEvent(payload, sig, secret, tolerance=300). 300 segundos e suficiente para producao, mas nao coloque muito grande (e sua janela de protecao contra replay).
Debug: imprima a diferenca now - t. Se for maior que 300, o relogio do servidor esta derivando. Sincronize NTP.
Causa 5: Secret commitado no codigo
O secret vaza, e um atacante forja webhooks com o secret real.
O fix: rode o secret imediatamente (env var de secret manager). Use git log -p mais grep -i whsec para encontrar vazamentos historicos.
Debug: trate o secret como uma senha — nunca deixe entrar no controle de versao.
Causa 6: Comparando assinaturas com ===
Funcionalmente funciona, mas teoricamente vulneravel a ataques de timing remotos. Atacantes enviam requests repetidos e medem diferencas de tempo de resposta para quebrar a assinatura byte a byte. O crypto.timingSafeEqual do Node e o hmac.compare_digest do Python sao comparacoes constant-time, imunes a isso.
O fix:
- Node: crypto.timingSafeEqual(Buffer.from(sig1), Buffer.from(sig2))
- Python: hmac.compare_digest(sig1, sig2)
Debug: grep o codigo por === e ==. Onde comparar assinaturas, troque pela versao timing-safe.
Causa 7: Secret vazado em logs durante debug
Funciona em local, falha para todos os webhooks em producao. Causa: voce logou o secret durante o debug (ou commitou), depois rodou. Os webhooks agora assinam com o novo secret, mas a env var de producao nao foi atualizada.
O fix: logue apenas o status de match, nao o secret nem a assinatura. console.log(sig verify, matched: true).
Debug: git log -p grep por palavras-chave do secret. Confirme que nenhum commit historico vazou.
Como reproduzir qualquer assinatura localmente
- Use a aba Compute do Validador de Assinatura de Webhook da Piick. Preencha secret, raw body e provedor, o header de assinatura gerado vai direto para sua config de webhook.
- Use a aba Verify para colar o header recebido. Veja se bate, feedback instantaneo, sem console.log nem restart de servico.
- Esta ferramenta e totalmente local (Web Crypto API do navegador), nao sobe payload / secret, segura para usar secrets reais durante debug.
Cheat sheet de formato de assinatura de 5 provedores
| Provedor | Header | Encoding | Payload assinado |
|---|---|---|---|
| Stripe | Stripe-Signature: t=[ts],v1=[hex] | hex | [ts].[raw body] |
| GitHub | X-Hub-Signature-256: sha256=[hex] | hex | raw body |
| Shopify | X-Shopify-Hmac-SHA256: [base64] | base64 | raw body |
| Slack | X-Slack-Signature: v0=[hex] | hex | v0:[ts]:[raw body] |
| Generic | customizado | hex | raw body |
- Shopify e o unico que usa base64, os demais usam hex
- So Stripe e Slack incluem timestamp
- So GitHub (sha256=) e Slack (v0=) tem prefixo
- Veja a referencia completa escolhendo um provedor em webhook-signature-validator. Cada um vem com um payload de exemplo integrado.
Praticas recomendadas
- Use express.raw() / request.get_data() para processar webhooks, nunca JSON.stringify(parsedBody)
- Siga o encoding do provedor a risca. Stripe / GitHub / Slack = hex, Shopify = base64
- Compare assinaturas com timingSafeEqual / compare_digest, nao com ===
- Configure tolerancia de timestamp de 5 minutos para Stripe para se defender de replay attacks
- Secrets sempre vao por env vars, nunca no codigo, nunca em logs
Quer verificar uma assinatura voce mesmo? Use o Validador de Assinatura de Webhook da Piick no seu navegador para computar e verificar assinaturas, todos os 5 provedores cobertos, os dados ficam no navegador. Combine com o Decodificador JWT para cobertura completa do seu stack de ferramentas de auth e seguranca de webhooks.