All guides

Guide · Payments

Stripe webhook and Resend fulfilment checklist for Next.js SaaS

Toolbound guide5 min read

A Stripe webhook is where a payment becomes a fulfilled order. The buyer should end with a stored purchase record, one delivery email, and a way for you to recover a failed send without asking them to pay again.

Quick answer

A production Stripe webhook in Next.js must verify the Stripe signature against the raw request body, process each event once, persist the purchase in Postgres, then trigger Resend delivery email and seller alerts. Store enough delivery status to retry a failed email without creating a second purchase.

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 send with Resend

Record the purchase in your database before you send the download or onboarding email. If Resend 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 the Resend delivery email, download link, and seller alert.
  • Store delivery status so failed fulfilment can be retried.

4. Respond fast and keep a recovery path

Stripe expects a quick 2xx. Do the minimum synchronously: verify, record, acknowledge, then keep email delivery 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 Resend 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.

Where does Resend fit in the webhook flow?

Persist the purchase first, then trigger the Resend delivery email and seller alert. Storing before sending means a failed email can be retried from a durable record instead of becoming a lost payment.

Next step

Start from a working 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.