Oferta startowa: −50 % na pierwszą płatność, do 30 listopada.Zobacz ofertę →
webhook·toolkit
Stripe

Weryfikacja podpisu webhooka Stripe nie powiodła się — sprawdź dlaczego

Ostatnia aktualizacja: lipiec 2026

constructEvent ze Stripe rzuca No signatures found matching the expected signature for payload, a Ty nie masz pojęcia, która z kilkunastu rzeczy poszła nie tak. Ta strona robi jedno: bierze trzy artefakty, które daje Ci Stripe — sekret whsec_, dokładne surowe body i nagłówek Stripe-Signature — przelicza HMAC i wskazuje tę jedną przyczynę, która faktycznie wszystko zepsuła.

W skrócie: w 9 przypadkach na 10 to surowe body (framework ponownie zserializował JSON przed weryfikacją) albo zły sekret podpisu (whsec_ z CLI zamiast tego z endpointu). Debugger poniżej odróżni jedno od drugiego po jednym wklejeniu.

Debuguj swój podpis Stripe

Stripe jest wybrany domyślnie. HMAC liczy się w Twojej przeglądarce za pomocą WebCrypto — Twój whsec_ nigdy nie opuszcza karty.

Wszystko działa w Twojej przeglądarce przez Web Crypto API. Twój sekret podpisu nigdy nie jest wysyłany na nasze serwery — sprawdź to w zakładce Sieć.

Anatomia nagłówka Stripe-Signature

Każdy webhook Stripe przychodzi z jednym nagłówkiem, który wygląda tak. Każde pole oddzielone przecinkiem ma swoje zadanie:

Prawdziwy nagłówek Stripe-Signature
Stripe-Signature: t=1614556800,
  v1=5257a869e7ecebeda32affa62cdca3fa51cad7414a563a8c6e3c4b41c2b7f2c8,
  v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
Anatomia nagłówka Stripe-Signature
t=Timestamp Unix (w sekundach) wygenerowania podpisu przez Stripe. Wchodzi w skład tego, co jest podpisywane — i to z nim porównuje się 5-minutowe okno ochrony przed replay.
v1=Podpis, który faktycznie weryfikujesz: HMAC-SHA256 z ciągu t + "." + rawBody, zakodowany w hex, z kluczem whsec_ Twojego endpointu. To jedyny schemat, który sprawdza constructEvent.
v0=Przestarzały schemat testowy. Stripe nadal go wysyła, ale biblioteka ignoruje go przy weryfikacji — nie próbuj dopasowywać do v0.
, (przecinek)Podczas rotacji sekretu Stripe podpisuje zarówno starym, jak i nowym sekretem, więc możesz zobaczyć dwie wartości v1=. constructEvent przechodzi, jeśli pasuje którakolwiek.

Podpisywany ciąg to `${t}.${rawBody}` — nie samo body. Pomiń prefiks t. we własnym weryfikatorze, a każdy podpis zawiedzie, nawet z właściwym sekretem.

6 głównych przyczyn, w kolejności sprawdzania

Uszeregowane według tego, jak często są winowajcą. Każda ma znak rozpoznawczy, który odróżnia ją od pozostałych.

#PrzyczynaCharakterystyczny objawRozwiązanie
1Body to nie surowe bajtyZawodzi na prawdziwych zdarzeniach, ale policzony HMAC jest blisko; przechodzi, jeśli wkleisz dokładny surowy ciąg do debuggera powyżej.Przekaż constructEvent nietknięte body żądania, zanim dotknie go jakikolwiek parser JSON.
2Zły sekret podpisuKażde zdarzenie zawodzi, HMAC nawet nie przypomina v1. Często to whsec_ z CLI użyty do ruchu z dashboardu.Skopiuj sekret ze strony endpointu w Dashboardzie (Developers → Webhooks), a nie ze stripe listen.
3Middleware sparsował body wcześniejDziała ze stripe trigger, ale na produkcji zwraca 400; express.json() w app.use Expressa uruchamia się przed Twoją trasą.Podepnij express.raw({type:'application/json'}) tylko na trasie webhooka, przed globalnym parserem JSON.
4Sekret z trybu testowego w trybie live (lub odwrotnie)Podpis zawodzi tylko przy prawdziwych płatnościach, a przy zdarzeniach testowych przechodzi. whsec_ wygląda dobrze, ale należy do drugiego trybu.Do ruchu live używaj whsec_ endpointu live; test i live mają każdy swój.
5Brakujący lub obcięty nagłóweksig jest undefined lub pusty; proxy albo CDN usunęło Stripe-Signature lub zmieniło go na małe litery.Czytaj req.headers['stripe-signature'] (małymi literami w Node) i upewnij się, że proxy go przekazuje.
6Sekret z innego endpointuJeden endpoint weryfikuje, drugi zwraca 400 — użyto tego samego whsec_ dla dwóch URL-i endpointów.Każdy endpoint ma własny sekret podpisu. Przypisz sekret do URL-a, który Stripe faktycznie wywołuje.

Poprawka surowego body, framework po frameworku

Przyczyna nr 1 to niemal zawsze to: Twój framework webowy odczytał, sparsował i ponownie zserializował JSON, zanim zobaczył go constructEvent. Ponownie zserializowany JSON ma inne białe znaki, więc HMAC przestaje się zgadzać. Oto jak przekazać każdemu frameworkowi nietknięte bajty.

FrameworkDlaczego body jest zmienianeRozwiązanie
Expressexpress.json() parsuje i odrzuca surowy tekst w całej aplikacji.express.raw({type:'application/json'}) na trasie webhooka, przed express.json().
Next.js App RouterRoute handlery nie parsują body za Ciebie.const body = await request.text() — ten ciąg jest już surowy.
Next.js Pages RouterWbudowany bodyParser przepisuje body.export const config = { api: { bodyParser: false } }, a potem zbuforuj strumień.
Flaskrequest.json / request.form zużywają strumień i kodują go ponownie.payload = request.get_data() — bajty, zanim dotkniesz .json.
DjangoDostęp do request.POST blokuje body.request.body — odczytaj je najpierw, przed jakimkolwiek dostępem do formularza.
AWS Lambda / API GatewayAPI Gateway może zakodować body w base64 albo mapping template może je przepisać.Użyj integracji Lambda proxy; jeśli event.isBase64Encoded, zdekoduj event.body przed weryfikacją.
Express — surowe body tylko na trasie webhooka
// Trasa webhooka MUSI być przed 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()); // cała reszta
Next.js App Router — app/api/webhook/route.ts
export async function POST(req: Request) {
  const body = await req.text();            // surowe, nietknięte
  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 — wyłącz 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 });
}

Wklej dokładne bajty do debuggera powyżej: jeśli podpis zgadza się dopiero po usunięciu końcowego znaku nowej linii albo przekodowanego pola, body zostało zmienione po drodze — a nie Twój sekret.

Tryb testowy a live: sekret, który wygląda dobrze, ale nie jest

Stripe daje Ci trzy różne wartości whsec_ i łatwo je pomylić. Pierwsza to sekret endpointu testowego, widoczny w Dashboardzie w trybie testowym. Druga to sekret endpointu live — osobna wartość tego samego endpointu przełączonego na live. Trzecia to sekret CLI, który wypisuje stripe listen, znowu inny i ważny tylko dla zdarzeń przekazywanych przez tę sesję. Wszystkie trzy dają wiarygodnie wyglądający HMAC, więc błąd jest ten sam, ogólny No signatures found — nic nie mówi o pomyleniu trybów. Znak rozpoznawczy: płatności testowe się weryfikują, prawdziwe dostają 400. Rozwiązanie: odczytaj sekret z dokładnie tego endpointu, w dokładnie tym trybie, który odbiera ruch.

5-minutowe okno ochrony przed replay, o którym wszyscy zapominają

constructEvent nie sprawdza tylko HMAC — odrzuca każde zdarzenie, którego timestamp t= jest starszy niż 300 sekund, rzucając Timestamp outside the tolerance zone. Daje to o sobie znać, gdy po kilku godzinach odtwarzasz przechwycone zdarzenie albo gdy zegar serwera się rozjechał. Podpis jest całkowicie poprawny; zawodzi wiek zdarzenia. Debugger powyżej pokazuje wiek timestampu, co pozwala odróżnić przeterminowany replay od prawdziwej niezgodności podpisu. Jeśli naprawdę potrzebujesz dłuższego okna, constructEvent przyjmuje czwarty argument tolerance w sekundach — ale najpierw napraw zegar.

Handler czy sekret? Wygeneruj zdarzenie, o którym wiesz, że jest poprawne

Gdy debugger mówi, że sekret jest dobry, a produkcja nadal zwraca 400, błąd tkwi w tym, jak Twój handler odczytuje body. Wyizoluj go: nasz Signer generuje payload podpisany jak przez Stripe sekretem, który kontrolujesz, więc możesz wysłać POST-em gwarantowanie poprawne zdarzenie prosto do lokalnego handlera. Jeśli przejdzie, Twój kod weryfikacji jest w porządku, a problemem jest sekret albo middleware. Jeśli nie, handler zmienia body — wróć do tabeli o surowym body powyżej.

Wygeneruj podpisane zdarzenie Stripe

Lista kontrolna diagnostyki

Wykonaj te kroki po kolei, gdy następnym razem constructEvent rzuci błąd.

  1. Wklej niepasujący whsec_, surowe body i Stripe-Signature do debuggera powyżej — wskaże przyczynę, zanim zaczniesz czytać dalej.
  2. Upewnij się, że body docierające do constructEvent to nietknięty surowy ciąg, a nie obiekt sparsowany i ponownie zserializowany.
  3. Skopiuj sekret podpisu prosto ze strony endpointu w Dashboardzie — nie ze stripe listen, nie z innego endpointu.
  4. Sprawdź, czy dla ruchu live używasz sekretu live, a dla ruchu testowego sekretu testowego.
  5. Zaloguj req.headers['stripe-signature'] i sprawdź, czy nagłówek jest obecny i nie został obcięty przez proxy.
  6. Sprawdź, czy timestamp t= zdarzenia nie jest starszy niż 5 minut (przeterminowany replay lub rozjechany zegar).

Przechwyć i odtwórz prawdziwe zdarzenie, które zawodzi

Nadal nic? Skieruj Stripe na Relay Webhook Toolkit albo URL do przechwytywania, złap dokładnie to żądanie, które zawodzi, i obejrzyj surowe body oraz nagłówki bajt po bajcie — a potem odtwarzaj je na swoim handlerze tyle razy, ile potrzebujesz, bez czekania na kolejne zdarzenie live.

Tester webhooków StripeWalidator podpisu webhookaRelay

Najczęstsze pytania

Dlaczego mój webhook Stripe działa lokalnie, a na produkcji nie?

Prawie zawsze chodzi o surowe body. Lokalnie ze stripe listen CLI przekazuje nietknięte bajty, ale Twój produkcyjny middleware (express.json() w Expressie, bodyParser w Pages Router, mapping template w API Gateway) parsuje i ponownie serializuje JSON przed weryfikacją. Przekaż constructEvent surowe body żądania, a przejdzie.

Co oznacza „No signatures found matching the expected signature for payload”?

Policzony HMAC nie pasuje do żadnego v1= wysłanego przez Stripe. Dwie typowe przyczyny: body zostało zmienione przed weryfikacją albo sekret podpisu jest zły (często whsec_ z CLI zamiast tego z endpointu). Wklej wszystkie trzy do debuggera powyżej, a powie Ci, która.

Czy mogę używać tego samego sekretu podpisu w trybie testowym i live?

Nie. Każdy endpoint ma osobny whsec_ dla trybu testowego i live, a CLI wypisuje trzeci. Sekret testowy daje wiarygodnie wyglądający HMAC, który i tak zawodzi przy zdarzeniach live, z tym samym ogólnym błędem. Dopasuj sekret do trybu, który odbiera ruch.

Jak naprawić „Timestamp outside the tolerance zone”?

Podpis jest poprawny, ale t= zdarzenia jest starszy niż 300 sekund — zwykle to odtworzone przechwycone zdarzenie albo rozjechany zegar serwera. Napraw zegar albo odtwórz świeże zdarzenie. W ostateczności przekaż większą tolerancję (w sekundach) jako czwarty argument constructEvent.

Czy v0 w nagłówku Stripe-Signature służy do weryfikacji?

Nie. constructEvent sprawdza tylko v1= (HMAC-SHA256 z t.rawBody). v0= to przestarzały schemat testowy, który Stripe nadal wysyła, ale biblioteka go ignoruje. Jeśli Twój własny weryfikator porównuje z v0, to jest Twój błąd.

Czy ten debugger podpisów Stripe wysyła gdzieś mój sekret?

Nie. HMAC liczy się w Twojej przeglądarce za pomocą WebCrypto. Twój whsec_, body i nagłówek nigdy nie opuszczają karty — nic nie jest wysyłane, logowane ani zapisywane. Odłącz się od sieci, a nadal będzie działać.

Oferta startowa · −50 %

Debugujesz integrację webhooków w tym tygodniu?

Pass na 7 dni odblokowuje wyjaśnienia AI i gotowy kod handlera, 25 stałych URL-i, 30 dni historii i tunel Relay do localhost. Jedna płatność, bez subskrypcji.

5 € 2,50 € jednorazowo
Płatność jednorazowa · bez subskrypcji · zwrot w ciągu 14 dni · cena startowa do 30 listopada
Weryfikacja podpisu webhooka Stripe nie powiodła się: 6 przyczyn · Webhook Toolkit