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

ProvedorHeaderEncodingPayload assinado
StripeStripe-Signature: t=[ts],v1=[hex]hex[ts].[raw body]
GitHubX-Hub-Signature-256: sha256=[hex]hexraw body
ShopifyX-Shopify-Hmac-SHA256: [base64]base64raw body
SlackX-Slack-Signature: v0=[hex]hexv0:[ts]:[raw body]
Genericcustomizadohexraw 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.