Oferta de lançamento: −50 % no seu primeiro pagamento, até 30 de novembro.Ver a oferta →
webhook·toolkit
Stripe

Verificação de assinatura de webhook da Stripe falhou — descubra por quê

Última atualização: julho de 2026

O constructEvent da Stripe lança No signatures found matching the expected signature for payload e você não faz ideia de qual de uma dúzia de coisas deu errado. Esta página faz uma coisa só: pega os três artefatos que a Stripe te entrega — o secret whsec_, o corpo bruto exato e o cabeçalho Stripe-Signature —, recalcula o HMAC e aponta a única causa que realmente quebrou a verificação.

Resumindo: 9 em cada 10 vezes é o corpo bruto (o seu framework re-serializou o JSON antes da verificação) ou o signing secret errado (o whsec_ da CLI em vez do secret do endpoint). O depurador abaixo distingue os dois com uma única colagem.

Depure a sua assinatura da Stripe

A Stripe vem selecionada por padrão. O HMAC é calculado no seu navegador com WebCrypto — o seu whsec_ nunca sai da aba.

Tudo roda no seu navegador com a Web Crypto API. Seu secret de assinatura nunca é enviado aos nossos servidores — confira na aba Rede.

Anatomia do cabeçalho Stripe-Signature

Todo webhook da Stripe chega com um cabeçalho parecido com este. Cada campo separado por vírgula tem uma função:

Um cabeçalho Stripe-Signature real
Stripe-Signature: t=1614556800,
  v1=5257a869e7ecebeda32affa62cdca3fa51cad7414a563a8c6e3c4b41c2b7f2c8,
  v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
Anatomia do cabeçalho Stripe-Signature
t=Timestamp Unix (em segundos) de quando a Stripe gerou a assinatura. Faz parte do que é assinado — e é a referência da checagem de replay de 5 minutos.
v1=A assinatura que você realmente verifica: HMAC-SHA256 da string t + "." + rawBody, em hex, com a chave whsec_ do seu endpoint. É o único esquema que o constructEvent verifica.
v0=Um esquema de teste legado. A Stripe ainda o envia, mas a biblioteca o ignora na verificação — não tente comparar com v0.
, (vírgula)Durante uma rotação de secret, a Stripe assina com o secret antigo e com o novo, então você pode ver dois valores v1=. O constructEvent passa se qualquer um deles bater.

A string assinada é `${t}.${rawBody}` — não o corpo sozinho. Esqueça o prefixo t. em um verificador feito à mão e toda assinatura falha, mesmo com o secret certo.

As 6 causas raiz, na ordem em que verificar

Ordenadas pela frequência com que são as culpadas. Cada uma tem um sinal que a distingue das outras.

#CausaSintoma característicoCorreção
1O corpo não são os bytes brutosFalha com eventos reais, mas o HMAC calculado chega perto; passa se você colar a string bruta exata no depurador acima.Passe ao constructEvent o corpo da requisição intacto, antes de qualquer parser JSON tocar nele.
2Signing secret erradoTodo evento falha, HMAC nem perto do v1. Muitas vezes é o whsec_ da CLI usado contra o tráfego do dashboard.Copie o secret da página do endpoint no Dashboard (Developers → Webhooks), não do stripe listen.
3Um middleware parseou o corpo antesFunciona com stripe trigger mas dá 400 em produção; o app.use(express.json()) do Express roda antes da sua rota.Monte express.raw({type:'application/json'}) apenas na rota do webhook, acima do parser JSON global.
4Secret do modo teste no modo live (ou vice-versa)A assinatura só falha em pagamentos reais, passa nos eventos de teste. O whsec_ parece certo, mas pertence ao outro modo.Use o whsec_ do endpoint live para o tráfego live; teste e live têm cada um o seu.
5Cabeçalho ausente ou truncadosig é undefined ou vazio; um proxy ou CDN removeu o Stripe-Signature ou o passou para minúsculas.Leia req.headers['stripe-signature'] (em minúsculas no Node) e garanta que o seu proxy o repasse.
6Secret do endpoint erradoUm endpoint verifica, outro dá 400 — você reutilizou um único whsec_ em duas URLs de endpoint.Cada endpoint tem o seu próprio signing secret. Associe o secret à URL que a Stripe está realmente chamando.

A correção do corpo bruto, framework por framework

A causa nº 1 é quase sempre esta: o seu framework web leu, parseou e re-serializou o JSON antes de o constructEvent vê-lo. JSON re-serializado tem espaçamento diferente, então o HMAC deixa de bater. Veja como entregar a cada framework os bytes intactos.

FrameworkPor que o corpo é alteradoA correção
ExpressO express.json() parseia e descarta o texto bruto no app inteiro.express.raw({type:'application/json'}) na rota do webhook, antes do express.json().
Next.js App RouterOs route handlers não parseiam o corpo para você.const body = await request.text() — essa string já é bruta.
Next.js Pages RouterO bodyParser embutido reescreve o corpo.export const config = { api: { bodyParser: false } }, depois faça buffer do stream.
Flaskrequest.json / request.form consomem e recodificam o stream.payload = request.get_data() — bytes, antes de tocar em .json.
DjangoAcessar request.POST trava o corpo.request.body — leia-o primeiro, antes de qualquer acesso ao formulário.
AWS Lambda / API GatewayO API Gateway pode codificar em base64, ou um mapping template pode reescrever o corpo.Use a integração proxy do Lambda; se event.isBase64Encoded, decodifique event.body antes de verificar.
Express — corpo bruto só na rota do webhook
// A rota do webhook PRECISA vir antes do express.json()
app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['stripe-signature'];
    const event = stripe.webhooks.constructEvent(
      req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
    );
    res.json({ received: true });
  }
);
app.use(express.json()); // todo o resto
Next.js App Router — app/api/webhook/route.ts
export async function POST(req: Request) {
  const body = await req.text();            // bruto, intacto
  const sig = req.headers.get('stripe-signature')!;
  const event = stripe.webhooks.constructEvent(
    body, sig, process.env.STRIPE_WEBHOOK_SECRET!
  );
  return Response.json({ received: true });
}
Next.js Pages Router — desative o body parser
import { buffer } from 'micro';
export const config = { api: { bodyParser: false } };

export default async function handler(req, res) {
  const raw = await buffer(req);
  const sig = req.headers['stripe-signature'];
  const event = stripe.webhooks.constructEvent(
    raw, sig, process.env.STRIPE_WEBHOOK_SECRET
  );
  res.json({ received: true });
}

Cole os bytes exatos no depurador acima: se a assinatura só bate depois de remover uma quebra de linha final ou um campo recodificado, o corpo foi alterado no caminho — o problema não é o seu secret.

Modo teste vs modo live: o secret que parece certo, mas não é

A Stripe te entrega três valores whsec_ diferentes, e é fácil confundi-los. Primeiro, o secret do endpoint de teste, mostrado no Dashboard enquanto você está em modo teste. Segundo, o secret do endpoint live — um valor separado no mesmo endpoint passado para live. Terceiro, o secret da CLI que o stripe listen imprime, diferente de novo e válido apenas para os eventos encaminhados por aquela sessão. Os três produzem um HMAC de aparência válida, então o erro é o mesmo No signatures found genérico — nada diz modo trocado. O sinal: pagamentos de teste verificam, os reais dão 400. Correção: leia o secret do endpoint exato, no modo exato, que está recebendo o tráfego.

A janela de replay de 5 minutos que todo mundo esquece

O constructEvent não verifica só o HMAC — ele rejeita qualquer evento cujo timestamp t= tenha mais de 300 segundos, lançando Timestamp outside the tolerance zone. Isso pega quando você reenvia um evento capturado horas depois, ou quando o relógio do servidor saiu de sincronia. A assinatura é perfeitamente válida; o que falha é a idade. O depurador acima mostra a idade do timestamp para você distinguir um replay velho de uma assinatura realmente divergente. Se você precisa mesmo de uma janela maior, o constructEvent aceita um quarto argumento tolerance em segundos — mas corrija o relógio primeiro.

É o seu handler ou o seu secret? Gere um evento sabidamente válido

Quando o depurador diz que o secret está certo mas a produção continua dando 400, o bug está em como o seu handler lê o corpo. Isole-o: o nosso Signer gera um payload assinado no formato da Stripe com um secret que você controla, para você fazer POST de um evento garantidamente válido direto no seu handler local. Se passar, o seu código de verificação está certo e o problema é o secret ou o middleware. Se falhar, o handler está alterando o corpo — volte à tabela do corpo bruto acima.

Gerar um evento Stripe assinado

Checklist de diagnóstico

Siga esta ordem na próxima vez que o constructEvent lançar um erro.

  1. Cole o whsec_, o corpo bruto e o Stripe-Signature que falham no depurador acima — ele aponta a causa antes de você continuar lendo.
  2. Confirme que o corpo que chega ao constructEvent é a string bruta intacta, não um objeto parseado e re-serializado.
  3. Copie o signing secret direto da página do endpoint no Dashboard — não do stripe listen, nem de outro endpoint.
  4. Verifique se você usa o secret live para o tráfego live e o secret de teste para o tráfego de teste.
  5. Registre req.headers['stripe-signature'] no log e confirme que ele está presente e não foi truncado por um proxy.
  6. Verifique se o timestamp t= do evento não tem mais de 5 minutos (replay velho ou relógio fora de sincronia).

Capture e reenvie o evento real que falha

Ainda travado? Aponte a Stripe para um Relay ou uma URL de captura do Webhook Toolkit, pegue a requisição exata que está falhando e inspecione o corpo bruto e os cabeçalhos byte a byte — depois reenvie-a ao seu handler quantas vezes precisar, sem esperar outro evento real.

Testador de webhook StripeValidador de assinatura de webhookRelay

Perguntas frequentes

Por que o meu webhook da Stripe funciona localmente mas não em produção?

Quase sempre é o corpo bruto. No dev local com stripe listen, a CLI encaminha os bytes intactos, mas o seu middleware de produção (express.json() do Express, um bodyParser do Pages Router, um mapping template do API Gateway) parseia e re-serializa o JSON antes da verificação. Passe ao constructEvent o corpo bruto da requisição e ele passa.

O que significa 'No signatures found matching the expected signature for payload'?

O HMAC que você calculou não bate com nenhum v1= enviado pela Stripe. Duas causas habituais: o corpo foi alterado antes da verificação, ou o signing secret está errado (muitas vezes o whsec_ da CLI em vez do secret do endpoint). Cole os três no depurador acima e ele diz qual.

Posso usar o mesmo signing secret no modo teste e no modo live?

Não. Cada endpoint tem um whsec_ separado para teste e para live, e a CLI imprime um terceiro. Um secret de teste gera um HMAC de aparência válida que ainda assim falha nos eventos live, com o mesmo erro genérico. Associe o secret ao modo que está recebendo o tráfego.

Como corrijo 'Timestamp outside the tolerance zone'?

A assinatura é válida, mas o t= do evento tem mais de 300 segundos — normalmente um replay de uma captura ou o relógio do servidor fora de sincronia. Corrija o relógio ou reenvie um evento novo. Em último caso, passe uma tolerance maior (em segundos) como quarto argumento do constructEvent.

O v0 do cabeçalho Stripe-Signature é usado na verificação?

Não. Só o v1= (HMAC-SHA256 de t.rawBody) é verificado pelo constructEvent. O v0= é um esquema de teste legado que a Stripe ainda emite, mas a biblioteca ignora. Se o seu verificador feito à mão compara com o v0, o bug é esse.

Este depurador de assinatura da Stripe envia o meu secret para algum lugar?

Não. O HMAC é calculado no seu navegador com WebCrypto. O seu whsec_, o corpo e o cabeçalho nunca saem da aba — nada é enviado, registrado ou armazenado. Desconecte da rede e ele continua funcionando.

Oferta de lançamento · −50 %

Depurando uma integração de webhook esta semana?

O Pass 7 dias desbloqueia as explicações com IA e o código do handler pronto para colar, 25 URLs permanentes, 30 dias de histórico e o túnel Relay para localhost. Um único pagamento, sem assinatura.

€ 5 € 2,50 uma vez
Pagamento único · sem assinatura · reembolso em 14 dias · preço de lançamento até 30 de novembro
Verificação de assinatura de webhook da Stripe falhou: 6 causas · Webhook Toolkit