Offerta di lancio: −50 % sul tuo primo pagamento, fino al 30 novembre.Vedi l'offerta →
webhook·toolkit
Stripe

Verifica della firma webhook Stripe fallita — scopri perché

Ultimo aggiornamento: luglio 2026

constructEvent di Stripe lancia No signatures found matching the expected signature for payload e non hai idea di quale tra una dozzina di cose sia andata storta. Questa pagina fa una cosa sola: prende i tre elementi che Stripe ti fornisce — il secret whsec_, il body grezzo esatto e l'header Stripe-Signature — ricalcola l'HMAC e indica l'unica causa che l'ha davvero rotta.

In breve: 9 volte su 10 è il body grezzo (il tuo framework ha ri-serializzato il JSON prima della verifica) o il secret di firma sbagliato (il whsec_ della CLI invece di quello dell'endpoint). Il debugger qui sotto distingue i due casi con un solo incolla.

Fai il debug della tua firma Stripe

Stripe è selezionato di default. L'HMAC viene calcolato nel tuo browser con WebCrypto — il tuo whsec_ non lascia mai la scheda.

Tutto gira nel tuo browser con la Web Crypto API. Il tuo secret di firma non viene mai inviato ai nostri server — verificalo nel tab Rete.

Anatomia dell'header Stripe-Signature

Ogni webhook Stripe arriva con un header che assomiglia a questo. Ogni campo separato da virgola ha un compito:

Un vero header Stripe-Signature
Stripe-Signature: t=1614556800,
  v1=5257a869e7ecebeda32affa62cdca3fa51cad7414a563a8c6e3c4b41c2b7f2c8,
  v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
Anatomia dell'header Stripe-Signature
t=Timestamp Unix (in secondi) del momento in cui Stripe ha generato la firma. Fa parte di ciò che viene firmato — ed è il valore con cui si confronta il controllo anti-replay di 5 minuti.
v1=La firma che verifichi davvero: HMAC-SHA256 della stringa t + "." + rawBody, codificato in hex, con chiave il whsec_ del tuo endpoint. È l'unico schema che constructEvent controlla.
v0=Uno schema di test legacy. Stripe lo invia ancora ma la libreria lo ignora in verifica — non provare a confrontarti con v0.
, (virgola)Durante una rotazione del secret Stripe firma sia con il vecchio sia con il nuovo secret, quindi puoi vedere due valori v1=. constructEvent passa se almeno uno corrisponde.

La stringa firmata è `${t}.${rawBody}` — non il body da solo. Dimentica il prefisso t. in un verificatore scritto a mano e ogni firma fallisce, anche con il secret giusto.

Le 6 cause alla radice, nell'ordine in cui controllarle

Ordinate per frequenza. Ognuna ha un indizio che la distingue dalle altre.

#CausaSintomo distintivoSoluzione
1Il body non sono i byte grezziFallisce sugli eventi reali ma l'HMAC calcolato è vicino; passa se incolli la stringa grezza esatta nel debugger qui sopra.Passa a constructEvent il body della richiesta intatto, prima che qualsiasi parser JSON lo tocchi.
2Secret di firma sbagliatoOgni evento fallisce, HMAC lontanissimo dal v1. Spesso è il whsec_ della CLI usato sul traffico della dashboard.Copia il secret dalla pagina dell'endpoint nella Dashboard (Developers → Webhooks), non da stripe listen.
3Un middleware ha parsato il body primaFunziona con stripe trigger ma dà 400 in produzione; app.use(express.json()) di Express gira prima della tua route.Monta express.raw({type:'application/json'}) solo sulla route del webhook, sopra il parser JSON globale.
4Secret di test in modalità live (o viceversa)La firma fallisce solo per i pagamenti reali, passa per gli eventi di test. Il whsec_ sembra giusto ma appartiene all'altra modalità.Usa il whsec_ dell'endpoint live per il traffico live; test e live hanno ciascuno il proprio.
5Header mancante o troncatosig è undefined o vuoto; un proxy o una CDN ha rimosso Stripe-Signature o l'ha messo in minuscolo.Leggi req.headers['stripe-signature'] (in minuscolo in Node) e assicurati che il tuo proxy lo inoltri.
6Secret dell'endpoint sbagliatoUn endpoint verifica, un altro dà 400 — hai riusato un solo whsec_ su due URL di endpoint.Ogni endpoint ha il proprio secret di firma. Associa il secret all'URL che Stripe sta effettivamente chiamando.

La correzione del body grezzo, framework per framework

La causa n. 1 è quasi sempre questa: il tuo framework web ha letto, parsato e ri-serializzato il JSON prima che constructEvent lo vedesse. Il JSON ri-serializzato ha spazi diversi, quindi l'HMAC non corrisponde più. Ecco come passare a ogni framework i byte intatti.

FrameworkPerché il body viene modificatoLa correzione
Expressexpress.json() parsa e scarta il testo grezzo in tutta l'app.express.raw({type:'application/json'}) sulla route del webhook, prima di express.json().
Next.js App RouterI route handler non parsano il body al posto tuo.const body = await request.text() — quella stringa è già grezza.
Next.js Pages RouterIl bodyParser integrato riscrive il body.export const config = { api: { bodyParser: false } }, poi bufferizza lo stream.
Flaskrequest.json / request.form consumano e ricodificano lo stream.payload = request.get_data() — byte, prima di toccare .json.
DjangoAccedere a request.POST blocca il body.request.body — leggilo per primo, prima di qualsiasi accesso al form.
AWS Lambda / API GatewayAPI Gateway può codificare in base64, o un mapping template può riscrivere il body.Usa la Lambda proxy integration; se event.isBase64Encoded, decodifica event.body prima di verificare.
Express — body grezzo solo sulla route del webhook
// La route del webhook DEVE venire prima di 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()); // tutto il resto
Next.js App Router — app/api/webhook/route.ts
export async function POST(req: Request) {
  const body = await req.text();            // grezzo, intatto
  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 — disattiva il 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 });
}

Incolla i byte esatti nel debugger qui sopra: se la firma corrisponde solo dopo aver tolto un a capo finale o un campo ricodificato, il body è stato modificato in transito — non è il tuo secret.

Test mode vs live mode: il secret che sembra giusto ma non lo è

Stripe ti dà tre valori whsec_ diversi ed è facile confonderli. Primo, il secret dell'endpoint di test, mostrato nella Dashboard quando sei in test mode. Secondo, il secret dell'endpoint live — un valore separato sullo stesso endpoint passato in live. Terzo, il secret della CLI che stampa stripe listen, ancora diverso e valido solo per gli eventi inoltrati da quella sessione. Tutti e tre producono un HMAC dall'aspetto valido, quindi l'errore è sempre il generico No signatures found — niente dice modalità sbagliata. L'indizio: i pagamenti di test si verificano, quelli reali danno 400. Soluzione: leggi il secret dall'endpoint esatto, nella modalità esatta, che riceve il traffico.

La finestra anti-replay di 5 minuti che tutti dimenticano

constructEvent non controlla solo l'HMAC — rifiuta qualsiasi evento il cui timestamp t= abbia più di 300 secondi, lanciando Timestamp outside the tolerance zone. Ti colpisce quando rinvii un evento catturato ore dopo, o quando l'orologio del tuo server è sfasato. La firma è perfettamente valida; è l'età a far fallire il controllo. Il debugger qui sopra mostra l'età del timestamp, così distingui un replay scaduto da una vera discrepanza di firma. Se ti serve davvero una finestra più lunga, constructEvent accetta un quarto argomento tolerance in secondi — ma prima sistema l'orologio.

È il tuo handler o il tuo secret? Genera un evento sicuramente valido

Quando il debugger dice che il secret è giusto ma la produzione dà ancora 400, il bug sta nel modo in cui il tuo handler legge il body. Isolalo: il nostro Signer genera un payload firmato Stripe con un secret che controlli tu, così puoi inviare in POST un evento sicuramente valido direttamente al tuo handler locale. Se passa, il tuo codice di verifica è a posto e il problema è il secret o il middleware. Se fallisce, l'handler sta modificando il body — torna alla tabella del body grezzo qui sopra.

Genera un evento Stripe firmato

Checklist di diagnosi

Esegui questi controlli in ordine la prossima volta che constructEvent lancia un'eccezione.

  1. Incolla nel debugger qui sopra il whsec_, il body grezzo e lo Stripe-Signature che falliscono — ti dà la causa prima che tu legga oltre.
  2. Verifica che il body che arriva a constructEvent sia la stringa grezza intatta, non un oggetto parsato e ri-serializzato.
  3. Copia il secret di firma direttamente dalla pagina dell'endpoint nella Dashboard — non da stripe listen, non da un altro endpoint.
  4. Verifica di usare il secret live per il traffico live e il secret di test per il traffico di test.
  5. Logga req.headers['stripe-signature'] e verifica che sia presente e non troncato da un proxy.
  6. Verifica che il timestamp t= dell'evento non abbia più di 5 minuti (replay scaduto o orologio sfasato).

Cattura e riesegui il vero evento che fallisce

Ancora bloccato? Punta Stripe su un Relay o su un URL di cattura di Webhook Toolkit, intercetta la richiesta esatta che fallisce e ispeziona body grezzo e header byte per byte — poi rieseguila sul tuo handler tutte le volte che vuoi, senza aspettare un altro evento live.

Stripe webhook testerValidatore di firma webhookRelay

Domande frequenti

Perché il mio webhook Stripe funziona in locale ma non in produzione?

Quasi sempre è il body grezzo. In locale con stripe listen la CLI inoltra i byte intatti, ma il tuo middleware di produzione (express.json() di Express, il bodyParser del Pages Router, un mapping template di API Gateway) parsa e ri-serializza il JSON prima della verifica. Passa a constructEvent il body grezzo della richiesta e la verifica va a buon fine.

Cosa significa «No signatures found matching the expected signature for payload»?

L'HMAC che hai calcolato non corrisponde a nessun v1= inviato da Stripe. Le due cause abituali: il body è stato modificato prima della verifica, oppure il secret di firma è sbagliato (spesso il whsec_ della CLI invece di quello dell'endpoint). Incolla tutti e tre gli elementi nel debugger qui sopra e ti dice quale.

Posso usare lo stesso secret di firma in test mode e in live mode?

No. Ogni endpoint ha un whsec_ separato per test e per live, e la CLI ne stampa un terzo. Un secret di test produce un HMAC dall'aspetto valido che fallisce comunque sugli eventi live, con lo stesso errore generico. Abbina il secret alla modalità che riceve il traffico.

Come risolvo «Timestamp outside the tolerance zone»?

La firma è valida ma il t= dell'evento ha più di 300 secondi — di solito un evento catturato e rinviato o un orologio del server sfasato. Sistema l'orologio, oppure rinvia un evento nuovo. Come ultima risorsa, passa una tolerance più ampia (in secondi) come quarto argomento di constructEvent.

Il v0 nell'header Stripe-Signature viene usato per la verifica?

No. constructEvent controlla solo v1= (HMAC-SHA256 di t.rawBody). v0= è uno schema di test legacy che Stripe emette ancora ma che la libreria ignora. Se il tuo verificatore scritto a mano confronta con v0, il bug è lì.

Questo debugger di firme Stripe invia il mio secret da qualche parte?

No. L'HMAC viene calcolato nel tuo browser con WebCrypto. Il tuo whsec_, il body e l'header non lasciano mai la scheda — nulla viene caricato, registrato o salvato. Scollegati dalla rete e funziona lo stesso.

Offerta di lancio · −50 %

Stai facendo il debug di un'integrazione webhook questa settimana?

Il Pass 7 giorni sblocca le spiegazioni AI e il codice dell'handler pronto da incollare, 25 URL permanenti, 30 giorni di cronologia e il tunnel Relay verso localhost. Un solo pagamento, nessun abbonamento.

5 € 2,50 € una volta
Pagamento unico · nessun abbonamento · rimborso entro 14 giorni · prezzo di lancio fino al 30 novembre
Verifica della firma webhook Stripe fallita: 6 cause · Webhook Toolkit