All guides

Guide · Payments

Next.js Stripe webhook checklist for SaaS launches

Toolbound guide5 min read

A Stripe webhook is where a payment becomes a fulfilled order. Get it wrong and you ship duplicate emails, missed deliveries, or fake purchases. These are the checks that decide whether your money path is trustworthy in production.

Quick answer

A production Stripe webhook in Next.js must do four things: verify the Stripe signature against the raw request body, handle each event idempotently so retries do not double-process, persist the purchase before fulfilling, and only then trigger delivery and alerts. Always read the raw body (not parsed JSON) for signature verification, and return 2xx quickly so Stripe does not retry a success.

1. Verify the signature against the raw body

Stripe signs each webhook with a secret. You must verify that signature against the exact raw request body, because any parsing or re-serialisation changes the bytes and breaks verification. In the Next.js App Router, read the raw text from the request before doing anything else.

  • Use the raw request body, not parsed JSON, for signature verification.
  • Verify with your webhook signing secret, not your API key.
  • Reject unverified events with a 400 and do not process them.

2. Make event handling idempotent

Stripe retries webhooks, so the same event can arrive more than once. Idempotency means processing the same event twice has no extra effect. Key off the Stripe event id or the checkout session id so a retry does not create a second purchase or send a second email.

  • Track processed event or session ids and skip duplicates.
  • Make purchase writes safe to repeat (upsert, not blind insert).
  • Return 2xx for events you have already handled.

3. Persist before you fulfil

Record the purchase in your database before you send the download or onboarding email. If fulfilment fails, you still have a durable record to recover from, and you avoid delivering against a payment you never stored.

  • Write the purchase and its metadata to Postgres first.
  • Then trigger delivery email, download link, and seller alert.
  • Keep an operational record so failed fulfilment can be retried.

4. Respond fast and fail safe

Stripe expects a quick 2xx. Do the minimum synchronously (verify, record, acknowledge) and keep slow work resilient. If something downstream breaks, your stored purchase lets you recover without asking the buyer to pay again.

  • Return a 2xx promptly so Stripe stops retrying a success.
  • Log failures with enough context to replay them.
  • Smoke-test the full path with Stripe test mode before launch.

FAQ

Common questions

Why must I use the raw body for Stripe signature verification?

Because Stripe signs the exact bytes it sent. If your framework parses the JSON and re-serialises it, the bytes change and verification fails. In Next.js, read the raw request text before parsing.

How do I stop a Stripe webhook from processing twice?

Make it idempotent: track the Stripe event id or checkout session id, skip anything already handled, and use upserts so repeated writes do not create duplicate purchases or emails.

Should I fulfil the order inside the webhook?

Persist the purchase inside the webhook first, then trigger fulfilment. Storing before delivering means a failed email or download can be retried from a durable record instead of a lost payment.

Next step

Start from working rails, not a blank repo.

Toolbound Stack is a buyer-owned Next.js boilerplate with Stripe, Resend, Postgres, setup checks, and agent handoff docs, so a coding agent can customise it safely.