All articles

May 8, 2026

How to set up Stripe webhooks in Next.js the right way

Webhooks are how Stripe tells your app what happened after checkout. Here is a complete guide to setting them up reliably in a Next.js app deployed on Cloudflare.

Most Stripe integration tutorials stop at the checkout session. They show you how to redirect a user to Stripe's payment page — but they skip the part that actually matters: knowing when the payment succeeded and updating your database.

That part is webhooks.

What webhooks are and why they matter

After a user completes checkout on Stripe's page, Stripe sends an HTTP POST request to a URL you configure. That request contains an event describing what happened — checkout.session.completed, customer.subscription.created, and so on.

Your app needs to handle these events to:

  • Grant access after a successful payment
  • Update subscription status when a plan changes
  • Handle failed payments and cancellations

Without webhooks, your app has no reliable way to know any of this happened.

The verification step most tutorials skip

Stripe signs every webhook request with a secret. Before processing any event, you must verify this signature. Skipping this check means anyone can send fake events to your endpoint and grant themselves access for free.

import Stripe from "stripe";

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

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  // Now safe to process the event
  switch (event.type) {
    case "checkout.session.completed":
      await handleCheckoutCompleted(event.data.object);
      break;
    case "customer.subscription.deleted":
      await handleSubscriptionCanceled(event.data.object);
      break;
  }

  return new Response("OK");
}

The key detail: you must read the raw request body as text before passing it to constructEvent. If you parse it as JSON first, the signature check fails.

Two things to be careful about on Cloudflare Workers

1. Use the Cloudflare-compatible Stripe client

The standard Stripe Node.js SDK makes assumptions about the Node.js runtime. On Cloudflare Workers, you need to initialize it with the correct fetch implementation:

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  httpClient: Stripe.createFetchHttpClient(),
});

Without this, Stripe requests will fail silently in production.

2. Respond before doing heavy work

Stripe expects a response within a few seconds. For operations that take longer, return a 200 OK immediately and do the work asynchronously using ctx.waitUntil().

Testing locally

Install the Stripe CLI and forward webhooks to your local development server:

stripe listen --forward-to localhost:3000/api/stripe/webhook

The CLI prints a webhook signing secret for local testing — use this as STRIPE_WEBHOOK_SECRET in your .env.local file.

The event types worth handling

For a typical SaaS product, these are the events you need:

| Event | What to do | |-------|-----------| | checkout.session.completed | Grant access, send welcome email | | customer.subscription.created | Record subscription in database | | customer.subscription.updated | Update plan tier | | customer.subscription.deleted | Downgrade to free plan | | invoice.payment_failed | Email the user, flag the account |

All of these are handled in the Flint source code, ready to customize.