Validador de assinatura de webhook
Seu provedor envia um cabeçalho de assinatura, seu handler diz que não bate, e a documentação responde com um SDK para instalar. Isso não te diz por quê. Cole a assinatura, seu secret e o corpo bruto abaixo: recalculamos o HMAC no seu navegador e, ao falhar, testamos as mutações que costumam causar isso — um espaço a mais no secret, uma quebra de linha final, um corpo re-serializado, um timestamp vencido.
Em resumo: uma assinatura de webhook falha quase sempre por uma de cinco razões — secret errado (test vs live), um corpo re-serializado pelo seu framework, um timestamp fora da janela de 5 minutos, uma quebra de linha final ou um prefixo sha256= / v0= que sobrou. Esta ferramenta recalcula o HMAC e diz qual é.
Cole uma assinatura que falha
Os quatro esquemas de assinatura
Os quatro são HMAC, mas diferem no que é hasheado e em como o resultado é codificado. Um detalhe errado e toda entrega falha.
| Provedor | O que é assinado | Algoritmo |
|---|---|---|
| Stripe | HMAC de t.body; assinatura e timestamp em um cabeçalho Stripe-Signature | SHA-256 hex |
| GitHub | HMAC do corpo bruto; assinatura em X-Hub-Signature-256 | SHA-256 hex |
| Shopify | HMAC do corpo bruto; assinatura em X-Shopify-Hmac-Sha256 | SHA-256 base64 |
| Slack | HMAC de v0:timestamp:body; também precisa do cabeçalho timestamp | SHA-256 hex |
O Twilio é a exceção — ele hasheia uma URL canônica mais parâmetros ordenados, não o corpo. Use o validador de assinatura do Twilio para isso.
As cinco causas reais
Ordenadas por quanto aparecem em threads de suporte.
| # | Causa | Sintoma | Correção |
|---|---|---|---|
| 1 | Secret errado | Toda entrega falha, mesmo uma nova | Confusão test vs live — cada provedor emite um secret de assinatura diferente por modo e por endpoint. |
| 2 | Corpo re-serializado | Falha depois que seu framework parseou o JSON | Assine os bytes brutos, não JSON.stringify(parsed): espaçamento e ordem das chaves mudam o hash. |
| 3 | Timestamp vencido | Passa nos testes, falha em eventos reenviados | Stripe e Slack rejeitam o que passa de 5 minutos. Não reenvie um evento capturado horas depois. |
| 4 | Quebra de linha final | Corpo deslocado um byte, assinatura nunca bate | Um proxy ou editor acrescentou uma quebra de linha. A ferramenta sinaliza esse caso exato. |
| 5 | Prefixo comparado literal | sha256= ou v0= incluído na comparação | Compare só a parte hex/base64, não o prefixo do esquema. |
A armadilha do corpo bruto
A causa número um: você verifica contra o corpo parseado-e-re-serializado em vez dos bytes exatos. JSON.stringify(JSON.parse(body)) muda espaçamento e ordem das chaves, então o HMAC não bate mais. Leia o corpo bruto antes de qualquer coisa parseá-lo.
// Express: keep the RAW bytes, do NOT let JSON re-serialise them
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body; // Buffer, exactly as received
verify(raw, req.headers['stripe-signature'], secret);
});# Flask: request.get_data() returns the raw body; request.json does NOT raw = request.get_data() # bytes, untouched verify(raw, request.headers['X-Hub-Signature-256'], secret)
// Next.js App Router: read the text before parsing
export async function POST(req) {
const raw = await req.text(); // sign THIS, not JSON.stringify(await req.json())
verify(raw, req.headers.get('stripe-signature'), secret);
}Regra prática: capture a assinatura e os bytes brutos na borda, verifique, depois parseie.
Timestamps e replays
Stripe e Slack embutem um timestamp na string assinada e rejeitam o que passa de cinco minutos — é proteção contra replay, não um bug. GitHub e Shopify não, então uma assinatura GitHub válida continua válida para sempre. Se a ferramenta diz válida mas expirada, seu secret e corpo estão bem; você só está reenviando um evento antigo. Gere uma entrega nova em vez de reenviar uma capturada.
Quando a assinatura confere
Precisa enviar um evento de teste corretamente assinado ao seu endpoint? O assinador de webhooks monta um para os seis provedores. Para capturar e inspecionar entregas reais, aponte-as para um túnel Relay.
Perguntas frequentes
Vocês guardam meu secret de assinatura?
Não. O HMAC é calculado no seu navegador com Web Crypto; o secret e o corpo nunca chegam aos nossos servidores. Abra a aba Rede: ao clicar em Verificar, nenhuma requisição sai.
Quais provedores são suportados?
Stripe, GitHub, Shopify e Slack — os quatro esquemas HMAC sobre o corpo. O Twilio assina uma URL canônica, então tem ferramenta própria: o validador de assinatura do Twilio.
Passa aqui mas meu servidor rejeita mesmo assim. Por quê?
Quase sempre o corpo. Seu framework parseou o JSON e você o re-serializou antes de verificar, o que muda espaçamento e ordem das chaves. Assine os bytes brutos — veja os trechos Express, Flask e Next acima.
O que significa 'válida mas expirada'?
O HMAC está correto, mas Stripe e Slack também recusam assinaturas com mais de cinco minutos para bloquear replays. Se você capturou um evento e o reenvia depois, a verificação falha no timestamp mesmo com o secret certo.