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 suite

Reference 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"}