Free Developer Tool
Stripe Webhook Signature Checker
Getting “No signatures found matching the expected signature for payload”? Paste the signing secret, the Stripe-Signature header and the raw body, and see whether they match and what changed if they don’t. Everything runs in your browser.
Runs entirely in your browser with the Web Crypto API. The secret, header and body are never sent anywhere.
How Stripe signs a webhook
- Stripe sends a
Stripe-Signatureheader liket=1767225600,v1=5257a8…: the timestamp, and onev1signature per active secret (two during a secret roll). - The signed string is the timestamp, a dot, and the raw request body exactly as sent:
1767225600.{"id": "evt_…", …}. - The signature is an HMAC-SHA256 of that string, keyed with the endpoint’s whole signing secret, including the
whsec_prefix, as hex. - Your server computes the same value and compares it with each
v1. Stripe’s libraries also reject timestamps more than five minutes old, to stop replays.
Why verification fails
- The wrong secret. Every endpoint has its own secret, and the one
stripe listenprints is different from the Dashboard endpoint’s. Both start withwhsec_; an API key (sk_…) never works. - A parsed body. If a JSON middleware runs before your webhook handler, you verify a re-serialized object, not the bytes Stripe signed. In Express, register the webhook route with
express.raw()beforeapp.use(express.json()). In the Next.js App Router, readawait req.text(). - A stale timestamp. Replaying a saved event, or a server clock that is minutes off, fails the five-minute tolerance even though the signature itself is right.
Express
javascript
// The webhook route comes before express.json()
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, // a Buffer with the raw body
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET,
);
// handle event.type ...
res.sendStatus(200);
});
app.use(express.json()); // other routesNext.js App Router
typescript
export async function POST(req: Request) {
const body = await req.text(); // raw body, not req.json()
const signature = req.headers.get("stripe-signature") ?? "";
const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
// handle event.type ...
return new Response(null, { status: 200 });
}Without the Stripe SDK (Node.js)
javascript
import crypto from "node:crypto";
export function verifyStripeSignature(rawBody, header, secret, toleranceSeconds = 300) {
const items = header.split(",").map((part) => part.split("="));
const t = items.find(([key]) => key === "t")?.[1];
const signatures = items.filter(([key]) => key === "v1").map(([, value]) => value);
if (!t || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return signatures.some(
(sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
);
}Next: which Stripe webhook events a SaaS should handle, with example payloads, and Stripe test cards to trigger them.
Built for Stripe
Your webhooks work. Now credit the affiliates behind each sale.
Refgrow connects to Stripe, verifies its own webhooks, and turns referred payments into commissions. A dry-run test mode shows which affiliate a test sale would be credited to, before any real customer pays.
- One-click Stripe connection, webhook created for you
- Checkout, subscriptions and one-time payments
- Dry-run test mode for attribution
- Payouts via PayPal and Wise