Skip to content

Checkout

  1. The cart holds { productId, variantId, quantity, options } per line and a destination country, and asks POST /api/shipping/quote for the rates that apply. Nothing displayable is stored; every price on the cart page is re-derived from the current catalogue on render.
  2. Checkout posts the same lines to POST /api/checkout. The server loads every referenced product from the live catalogue, reads each variant’s price from the database, checks stock, refuses anything not published to Stripe, sums the subtotal, resolves shipping again from the same function the cart used, and creates a Checkout Session with allow_promotion_codes, address collection if any line is physical, automatic_tax if tax is on, and the store’s PUBLIC_URL as success and cancel targets.
  3. The shopper pays on Stripe’s page.
  4. Stripe redirects to /confirm?session_id=…. The page polls GET /api/checkout/:sessionId and shows the order once its status is paid.
  5. Stripe’s webhook delivers checkout.session.completed. The handler records the order, decrements inventory, links it to a verified customer account if one matches, sends the confirmation email, and queues an order.paid outbound event.

Step 5 can arrive before or after step 4. The confirmation page copes with either.

  • Line items are built server-side from stored price ids. The client sends identifiers and quantities and never a price, so a tampered cart cannot change what anything costs. compareAtPriceCents never enters the calculation.
  • The webhook is the only thing that marks an order paid. The success redirect proves nothing: a buyer can close the tab, and the URL can be visited directly.
  • Webhook delivery is at-least-once, so events are deduplicated by id, and a failed handler releases the dedup record so Stripe’s retry is processed rather than dismissed.

Checked when the session is built and again, authoritatively, when payment is confirmed. If it ran out in between, the money has been taken, so the order is recorded and flagged oversold for the merchant rather than dropped. The quantity control in the storefront clamps against stock on every change, including a re-clamp when the variant changes, so the shopper finds out before payment rather than at it.

The cart asks for the destination country because hosted Checkout collects the address after the session exists, and a zone-priced store has to know the destination before then. The session is then restricted to that country, so a buyer cannot hold a domestic rate against an international address. A cart with no physical line collects no address at all. Shipping.

With no zones configured, Stripe is offered a small default list. With zones, the countries the zones name, plus every Stripe-shippable country if there is a catch-all zone.

Guest checkout is the default and nothing forces an account. The cart offers signing in and prefills the shipping country from a signed-in buyer’s default address. A guest order under an email that belongs to a verified account is linked to that account by the webhook, never to an unverified one.

stripe listen, the printed secret in .env, an API restart, and card 4242 4242 4242 4242. Replaying a delivered event with stripe events resend must not move stock a second time.