Launch-Angebot: −50 % auf deine erste Zahlung, bis 30. November.Zum Angebot →
webhook·toolkit
Stripe

Stripe-Webhook-Signaturprüfung fehlgeschlagen — finde heraus, warum

Zuletzt aktualisiert: Juli 2026

Stripes constructEvent wirft No signatures found matching the expected signature for payload, und du hast keine Ahnung, welche von einem Dutzend Möglichkeiten schiefgegangen ist. Diese Seite macht genau eine Sache: Sie nimmt die drei Artefakte, die Stripe dir gibt — das whsec_-Secret, den exakten Rohbody und den Stripe-Signature-Header —, berechnet den HMAC neu und zeigt auf die eine Ursache, die es wirklich kaputt gemacht hat.

Kurz gesagt: In 9 von 10 Fällen ist es der Rohbody (dein Framework hat das JSON vor der Prüfung neu serialisiert) oder das falsche Signing-Secret (das whsec_ der CLI statt das des Endpoints). Der Debugger unten unterscheidet die beiden mit einem einzigen Einfügen.

Deine Stripe-Signatur debuggen

Stripe ist standardmäßig ausgewählt. Der HMAC wird in deinem Browser mit WebCrypto berechnet — dein whsec_ verlässt den Tab nie.

Alles läuft im Browser mit der Web-Crypto-API. Ihr Signing-Secret wird nie an unsere Server gesendet — prüfen Sie es im Netzwerk-Tab.

Anatomie des Stripe-Signature-Headers

Jeder Stripe-Webhook kommt mit einem Header an, der so aussieht. Jedes kommagetrennte Feld hat eine Aufgabe:

Ein echter Stripe-Signature-Header
Stripe-Signature: t=1614556800,
  v1=5257a869e7ecebeda32affa62cdca3fa51cad7414a563a8c6e3c4b41c2b7f2c8,
  v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
Anatomie des Stripe-Signature-Headers
t=Unix-Timestamp (Sekunden), zu dem Stripe die Signatur erzeugt hat. Er ist Teil dessen, was signiert wird — und das, womit die 5-Minuten-Replay-Prüfung vergleicht.
v1=Die Signatur, die du tatsächlich prüfst: HMAC-SHA256 des Strings t + "." + rawBody, hex-kodiert, mit dem whsec_ deines Endpoints als Schlüssel. Das ist das einzige Schema, das constructEvent prüft.
v0=Ein veraltetes Test-Schema. Stripe sendet es noch, aber die Library ignoriert es bei der Prüfung — versuch nicht, gegen v0 zu matchen.
, (Komma)Während einer Secret-Rotation signiert Stripe mit dem alten und dem neuen Secret, du kannst also zwei v1=-Werte sehen. constructEvent ist erfolgreich, sobald einer davon passt.

Der signierte String ist `${t}.${rawBody}` — nicht der Body allein. Vergisst du in einem selbstgebauten Verifier das Präfix t., scheitert jede Signatur, selbst mit dem richtigen Secret.

Die 6 Grundursachen, in der Reihenfolge, in der du sie prüfen solltest

Sortiert danach, wie oft sie der Übeltäter sind. Jede hat ein Merkmal, das sie von den anderen unterscheidet.

#UrsacheTypisches SymptomLösung
1Body ist nicht die RohbytesScheitert bei echten Events, aber der berechnete HMAC ist nah dran; klappt, wenn du den exakten Roh-String in den Debugger oben einfügst.Gib constructEvent den unveränderten Request-Body, bevor irgendein JSON-Parser ihn anfasst.
2Falsches Signing-SecretJedes Event scheitert, der HMAC ist weit vom v1 entfernt. Oft das whsec_ der CLI, verwendet für Dashboard-Traffic.Kopiere das Secret von der Seite des Endpoints im Dashboard (Developers → Webhooks), nicht aus stripe listen.
3Middleware hat den Body zuerst geparstKlappt mit stripe trigger, liefert aber in Produktion 400; Express app.use(express.json()) läuft vor deiner Route.Hänge express.raw({type:'application/json'}) nur an die Webhook-Route, oberhalb des globalen JSON-Parsers.
4Test-Secret im Live-Modus (oder umgekehrt)Die Signatur scheitert nur bei echten Zahlungen, Test-Events klappen. Das whsec_ sieht richtig aus, gehört aber zum anderen Modus.Nutze für Live-Traffic das whsec_ des Live-Endpoints; Test und Live haben jeweils ein eigenes.
5Header fehlt oder ist abgeschnittensig ist undefined oder leer; ein Proxy oder CDN hat Stripe-Signature entfernt oder kleingeschrieben.Lies req.headers['stripe-signature'] (in Node kleingeschrieben) und stell sicher, dass dein Proxy den Header weiterleitet.
6Secret des falschen EndpointsEin Endpoint verifiziert, ein anderer liefert 400 — du hast ein einziges whsec_ für zwei Endpoint-URLs wiederverwendet.Jeder Endpoint hat sein eigenes Signing-Secret. Ordne das Secret der URL zu, die Stripe tatsächlich aufruft.

Der Rohbody-Fix, Framework für Framework

Ursache Nr. 1 ist fast immer diese: Dein Web-Framework hat das JSON gelesen, geparst und neu serialisiert, bevor constructEvent es zu sehen bekam. Neu serialisiertes JSON hat andere Leerzeichen, also passt der HMAC nicht mehr. So gibst du jedem Framework die unveränderten Bytes.

FrameworkWarum der Body verändert wirdDie Lösung
Expressexpress.json() parst den Rohtext appweit und verwirft ihn.express.raw({type:'application/json'}) auf der Webhook-Route, vor express.json().
Next.js App RouterRoute Handler parsen den Body nicht für dich.const body = await request.text() — dieser String ist bereits roh.
Next.js Pages RouterDer eingebaute bodyParser schreibt den Body um.export const config = { api: { bodyParser: false } }, dann den Stream puffern.
Flaskrequest.json / request.form verbrauchen den Stream und kodieren ihn neu.payload = request.get_data() — Bytes, bevor du .json anfasst.
DjangoEin Zugriff auf request.POST sperrt den Body.request.body — zuerst lesen, vor jedem Formularzugriff.
AWS Lambda / API GatewayAPI Gateway kann Base64-kodieren, oder ein Mapping Template schreibt den Body um.Lambda-Proxy-Integration verwenden; wenn event.isBase64Encoded, event.body vor der Prüfung dekodieren.
Express — Rohbody nur auf der Webhook-Route
// Die Webhook-Route MUSS vor express.json() kommen
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()); // alles andere
Next.js App Router — app/api/webhook/route.ts
export async function POST(req: Request) {
  const body = await req.text();            // roh, unverändert
  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 — Body-Parser deaktivieren
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 });
}

Füg die exakten Bytes in den Debugger oben ein: Passt die Signatur erst, wenn du einen abschließenden Zeilenumbruch oder ein neu kodiertes Feld entfernst, wurde der Body unterwegs verändert — nicht dein Secret.

Testmodus vs. Live-Modus: das Secret, das richtig aussieht, es aber nicht ist

Stripe gibt dir drei verschiedene whsec_-Werte, und man verwechselt sie leicht. Erstens das Secret des Test-Endpoints, das im Dashboard angezeigt wird, solange du im Testmodus bist. Zweitens das Secret des Live-Endpoints — ein separater Wert am selben Endpoint, auf Live umgeschaltet. Drittens das CLI-Secret, das stripe listen ausgibt, wieder ein anderes und nur für Events gültig, die diese Session weiterleitet. Alle drei erzeugen einen plausibel aussehenden HMAC, der Fehler ist also derselbe generische No signatures found — nichts sagt Modus-Verwechslung. Das Erkennungszeichen: Testzahlungen werden verifiziert, echte liefern 400. Die Lösung: Lies das Secret genau von dem Endpoint, im genau dem Modus, der den Traffic empfängt.

Das 5-Minuten-Replay-Fenster, das alle vergessen

constructEvent prüft nicht nur den HMAC — es lehnt jedes Event ab, dessen t=-Timestamp älter als 300 Sekunden ist, und wirft Timestamp outside the tolerance zone. Das trifft dich, wenn du ein erfasstes Event Stunden später erneut sendest oder die Uhr deines Servers abgedriftet ist. Die Signatur ist völlig gültig; es ist das Alter, das scheitert. Der Debugger oben zeigt das Alter des Timestamps, damit du ein veraltetes Replay von einer echten Signaturabweichung unterscheiden kannst. Brauchst du wirklich ein längeres Fenster, nimmt constructEvent ein viertes Argument tolerance in Sekunden — aber korrigiere zuerst die Uhr.

Dein Handler oder dein Secret? Erzeuge ein garantiert gültiges Event

Sagt der Debugger, dass das Secret stimmt, die Produktion aber trotzdem 400 liefert, liegt der Bug darin, wie dein Handler den Body liest. Grenze ihn ein: Unser Signer erzeugt eine von Stripe signierte Payload mit einem Secret, das du kontrollierst, sodass du ein garantiert gültiges Event direkt per POST an deinen lokalen Handler schicken kannst. Klappt es, ist dein Verifikationscode in Ordnung, und das Problem ist das Secret oder die Middleware. Scheitert es, verändert der Handler den Body — zurück zur Rohbody-Tabelle oben.

Ein signiertes Stripe-Event erzeugen

Diagnose-Checkliste

Geh diese Punkte der Reihe nach durch, wenn constructEvent das nächste Mal eine Exception wirft.

  1. Füg das fehlschlagende whsec_, den Rohbody und Stripe-Signature in den Debugger oben ein — er nennt die Ursache, bevor du weiterliest.
  2. Stell sicher, dass der Body, der bei constructEvent ankommt, der unveränderte Roh-String ist und kein geparstes und neu serialisiertes Objekt.
  3. Kopiere das Signing-Secret direkt von der Seite des Endpoints im Dashboard — nicht aus stripe listen, nicht von einem anderen Endpoint.
  4. Prüfe, dass du für Live-Traffic das Live-Secret und für Test-Traffic das Test-Secret verwendest.
  5. Logge req.headers['stripe-signature'] und stell sicher, dass der Header vorhanden ist und nicht von einem Proxy abgeschnitten wurde.
  6. Prüfe, dass der t=-Timestamp des Events nicht älter als 5 Minuten ist (veraltetes Replay oder Uhrendrift).

Das echte fehlschlagende Event erfassen und erneut abspielen

Kommst du immer noch nicht weiter? Richte Stripe auf ein Webhook-Toolkit-Relay oder eine Capture-URL, fang genau den Request ab, der scheitert, und inspiziere Rohbody und Headers Byte für Byte — dann spiel ihn so oft gegen deinen Handler ab, wie du willst, ohne auf ein weiteres Live-Event zu warten.

Stripe Webhook TesterWebhook-Signatur-ValidatorRelay

Häufige Fragen

Warum funktioniert mein Stripe-Webhook lokal, aber nicht in Produktion?

Fast immer der Rohbody. Lokal mit stripe listen leitet die CLI unveränderte Bytes weiter, aber deine Produktions-Middleware (Express express.json(), ein bodyParser im Pages Router, ein Mapping Template in API Gateway) parst das JSON und serialisiert es vor der Prüfung neu. Gib constructEvent den rohen Request-Body, und es klappt.

Was bedeutet „No signatures found matching the expected signature for payload“?

Der HMAC, den du berechnet hast, passt zu keinem der v1=, die Stripe gesendet hat. Zwei übliche Ursachen: Der Body wurde vor der Prüfung verändert, oder das Signing-Secret ist falsch (oft das whsec_ der CLI statt das des Endpoints). Füg alle drei in den Debugger oben ein, und er sagt dir, welche.

Kann ich dasselbe Signing-Secret für Test- und Live-Modus verwenden?

Nein. Jeder Endpoint hat ein eigenes whsec_ für Test und für Live, und die CLI gibt ein drittes aus. Ein Test-Secret erzeugt einen plausibel aussehenden HMAC, der bei Live-Events trotzdem scheitert, mit demselben generischen Fehler. Nimm das Secret des Modus, der den Traffic empfängt.

Wie behebe ich „Timestamp outside the tolerance zone“?

Die Signatur ist gültig, aber das t= des Events ist älter als 300 Sekunden — meist ein erneut abgespielter Capture oder eine abgedriftete Server-Uhr. Korrigiere die Uhr oder spiel ein frisches Event ab. Als letzten Ausweg übergib eine größere tolerance (in Sekunden) als viertes Argument an constructEvent.

Wird v0 im Stripe-Signature-Header zur Prüfung verwendet?

Nein. constructEvent prüft nur v1= (HMAC-SHA256 von t.rawBody). v0= ist ein veraltetes Test-Schema, das Stripe noch sendet, das die Library aber ignoriert. Matcht dein selbstgebauter Verifier gegen v0, ist genau das dein Bug.

Sendet dieser Stripe-Signatur-Debugger mein Secret irgendwohin?

Nein. Der HMAC wird in deinem Browser mit WebCrypto berechnet. Dein whsec_, Body und Header verlassen den Tab nie — nichts wird hochgeladen, geloggt oder gespeichert. Trenn die Netzwerkverbindung, und es funktioniert trotzdem.

Launch-Angebot · −50 %

Debuggst du diese Woche eine Webhook-Integration?

Der 7-Tage-Pass schaltet KI-Erklärungen und fertigen Handler-Code frei, dazu 25 dauerhafte URLs, 30 Tage Verlauf und den Relay-Tunnel zu localhost. Einmal zahlen, kein Abo.

5 € 2,50 € einmalig
Einmalzahlung · kein Abo · 14 Tage Geld-zurück · Launch-Preis bis 30. November
Stripe-Webhook-Signaturprüfung fehlgeschlagen: 6 Ursachen · Webhook Toolkit