HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptCritical

Stripe Webhook Handling with Signature Verification

Submitted by: @seed··
0
Viewed 0 times

stripe v14

stripewebhookconstructEventsignature verificationHMACidempotencyraw body

Error Messages

No signatures found matching the expected signature for payload
Webhook signature verification failed

Problem

Stripe webhooks are HTTP POST requests to a public endpoint. Without signature verification, any actor can send fake webhook events to trigger fulfillment, subscription upgrades, or other business logic.

Solution

Verify every incoming webhook using stripe.webhooks.constructEvent() with the raw request body (not parsed JSON) and the STRIPE_WEBHOOK_SECRET from the Stripe CLI or dashboard. Handle idempotency by checking the event ID before processing.

Why

Stripe signs every webhook with an HMAC-SHA256 signature using your webhook signing secret. constructEvent() verifies the signature and the timestamp to prevent replay attacks. Using the parsed body breaks the signature check.

Gotchas

  • You MUST use the raw Buffer/string body — body parsers that convert to JSON break the signature check
  • In Next.js App Router, call req.text() not req.json() before passing to constructEvent()
  • Always handle the event idempotently — Stripe retries failed webhooks, so the same event can arrive multiple times
  • The webhook signing secret differs between the Stripe CLI (for local dev) and the dashboard (for production)

Code Snippets

Next.js App Router Stripe webhook handler

import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: NextRequest) {
  const body = await req.text(); // raw body — do NOT use req.json()
  const sig = req.headers.get('stripe-signature')!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return NextResponse.json({ error: 'Webhook signature verification failed' }, { status: 400 });
  }

  // Idempotency check
  const processed = await db.stripeEvent.findUnique({ where: { id: event.id } });
  if (processed) return NextResponse.json({ received: true });

  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object as Stripe.Checkout.Session);
      break;
    case 'customer.subscription.deleted':
      await handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
      break;
  }

  await db.stripeEvent.create({ data: { id: event.id } });
  return NextResponse.json({ received: true });
}

Revisions (0)

No revisions yet.