CodeKits.devNew developer offer

Stripe Checkout and Verified Webhooks

A practical walkthrough of stripe checkout and verified webhooks, with code excerpts, failure checks and integration guidance.

By Jonathan · 5 min read

Published

Stripe Checkout and Verified Webhooks tutorial cover
In this article

This walkthrough covers Stripe Checkout and Verified Webhooks. A Next.js server, a Stripe test environment, an authenticated account system and an orders database. Use test credentials and test payment methods throughout the exercise.

The video is an illustrated code walkthrough with AI narration. Code blocks are integration excerpts, not a complete tested application. Helper names such as stores, middleware and clients describe responsibilities you must implement in your existing project. No live server, payment provider or device execution is claimed.

Before you begin

Use a separate development environment, fictional data and test credentials. Record dependency versions and follow the official references below for your installed versions. Keep a working baseline and add one workflow at a time.

1. Keep pricing on the server

The client can request a product and quantity, but it must not set the amount the server trusts. Resolve permitted prices and quantity limits from server-controlled data. Create an internal order connected to the authenticated user. Keep Stripe secret credentials on the server. The browser receives only the information needed to redirect into the hosted checkout flow.

Keep pricing on the server
POST /api/checkout
Client sends: productId, quantity
Server resolves: allowed Stripe price ID
Server records: orderId, userId, expected amount/currency

2. Create a Checkout Session

Create the Checkout Session with a server-selected price and trusted redirect destinations. Attach an internal order reference so later events can be reconciled. Reuse a stable idempotency key for retries of the same logical creation attempt. Persist the returned session identifier against the order. Don't treat the browser arriving at the success page as proof that payment has been received.

Create a Checkout Session
const session = await stripe.checkout.sessions.create({
 mode: "payment",
 line_items: [{price: allowedPriceId, quantity}],
 success_url: APP_URL + "/orders/" + order.id,
 cancel_url: APP_URL + "/checkout",
 metadata: {orderId: String(order.id)}
}, {idempotencyKey: "checkout:" + order.id});

3. Verify the original webhook body

Webhook signature verification uses the original request body and the endpoint's signing secret. Don't parse and reconstruct the J SON before verifying it. Reject invalid signatures without changing order state. The webhook signing secret is distinct from the API secret key and may differ between local forwarding and the deployed endpoint. Keep both out of logs and client bundles.

Verify the original webhook body
const payload = await request.text();
const signature = request.headers.get("stripe-signature");
let event;
try {
 event = stripe.webhooks.constructEvent(
  payload, signature, process.env.STRIPE_WEBHOOK_SECRET
 );
} catch { return new Response("Invalid signature", {status: 400}); }

4. Handle duplicate events

Webhook deliveries can repeat. Record the event identifier and the business state change in one transaction so a retry can't apply the same effect twice. A duplicate event should be acknowledged safely. Validate that the session belongs to the expected order and account, and compare the amount and currency with your server record. Store fulfillment work in a durable queue or outbox rather than losing it after acknowledgement.

Handle duplicate events
BEGIN;
INSERT INTO processed_events(event_id) VALUES (?);
-- Unique event_id prevents duplicate processing.
-- Validate order, session, amount, currency and payment state.
-- Update order and append an outbox job.
COMMIT;

5. Account for delayed payments

Some payment methods complete asynchronously. A completed checkout session doesn't always mean money is confirmed immediately. Build a small order state machine that handles pending, paid, and failed outcomes without moving backwards because events arrive out of order. Use the verified provider state and your own order reference. Fulfillment should happen once, only after the payment conditions for the product are satisfied.

Account for delayed payments
Relevant events include:
checkout.session.completed
checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
// Fulfill only after the payment is confirmed paid.

6. Test with local event forwarding

Use Stripe's test environment and local event forwarding while developing. Configure the signing secret belonging to the local listener. Exercise a real test checkout as well as intentionally invalid requests. Test duplicate delivery, database failure, delayed success, and retry behaviour. A mocked signature verifier doesn't prove the deployed route preserves the raw body, so verify the full path before enabling live payments.

Test with local event forwarding
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Use the signing secret printed for this listener.
# Complete a test checkout and inspect the order record.

7. Make order pages trustworthy

The success page should load the order state from your server and enforce ownership. It may initially show pending while the webhook arrives. Do not unlock a download or service based on a query parameter supplied by the browser. Add reconciliation for orders that remain pending unexpectedly, and keep support and refund workflows separate from the initial checkout implementation.

Make order pages trustworthy
GET /api/orders/:id
1. Authenticate account
2. Verify order ownership
3. Read server payment state
4. Show pending / paid / failed

Integration checks

Test bad signatures, modified payloads, duplicate events, incorrect amounts, another user’s order, delayed payment, database rollback and webhook redelivery. No live charges are made by these teaching excerpts.

These are verification instructions, not reported test results. The excerpts have not been executed as a complete integrated application. Review authentication, database schema, dependency versions and operational settings before deployment.

Official references

Explore CodeKits development services.