Provider standard
Stripe webhook signature verification
Stripe signs every delivery. Your receiver must verify stripe-signature against the raw request body before processing anything. Official documentation.
Test your Stripe receiver now
The free verification suite dispatches all eight vectors — valid signature, tampered body, corrupted signature, expired and future timestamps, missing header, rapid replay, and malformed JSON — against your endpoint in seconds.
Run the verification suiteReference implementations
Correct verification code for Stripe webhooks, per framework.
Next.js App Router (app/api/webhooks/stripe/route.ts)
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: NextRequest) {
const signature = req.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 });
}
// 1. Read unparsed raw text to preserve exact bytes for HMAC SHA256
const rawBody = await req.text();
let event: Stripe.Event;
try {
// 2. Pass explicit 300s tolerance window to prevent replay attacks
event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
300
);
} catch (err: any) {
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
// 3. Process event idempotently
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
// Handle completed session...
}
return NextResponse.json({ received: true });
}Express.js (routes/webhooks.ts)
import express, { Request, Response } from "express";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const router = express.Router();
// CRITICAL: Mount express.raw() BEFORE express.json() for this route
router.post(
"/api/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req: Request, res: Response) => {
const signature = req.headers["stripe-signature"] as string;
if (!signature) {
return res.status(400).send("Missing stripe-signature");
}
try {
const event = stripe.webhooks.constructEvent(
req.body, // Buffer from express.raw
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
300
);
// Handle event...
return res.status(200).json({ received: true });
} catch (err: any) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
export default router;FastAPI / Python (routes/webhook.py)
from fastapi import FastAPI, Request, HTTPException, Header
import stripe
import os
app = FastAPI()
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
@app.post("/api/webhooks/stripe")
async def stripe_webhook(request: Request, stripe_signature: str = Header(None)):
if not stripe_signature:
raise HTTPException(status_code=400, detail="Missing stripe-signature")
# Read raw body bytes
payload = await request.body()
try:
event = stripe.Webhook.construct_event(
payload=payload,
sig_header=stripe_signature,
secret=endpoint_secret,
tolerance=300
)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid signature: {str(e)}")
# Process event...
return {"status": "success"}