Ship/Now

What is wired · 3 min

Payments

Stripe Checkout, an idempotent webhook, and why the price never comes from the browser.

Money moves in two places and nowhere else: a route that opens a Checkout session, and a webhook that decides a sale happened.

Opening checkout

src/app/api/checkout/route.ts takes a plan id. That is all it takes.

const { data: plan } = await admin
  .from("plans").select("*").eq("id", planId).single();

const session = await stripe().checkout.sessions.create({
  mode: "payment",
  line_items: [{ quantity: 1, price_data: {
    currency: plan.currency,
    unit_amount: plan.price_cents,
    tax_behavior: "exclusive",
    product_data: { name: plan.name, tax_code: "txcd_10000000" },
  }}],
  automatic_tax: { enabled: true },
  metadata: { plan_id: plan.id, user_id: user?.id ?? "" },
  success_url: `${origin}/app?welcome=1`,
  cancel_url: `${origin}/#pricing`,
});

The price is read from the database. It is never taken from the request body, because a posted amount is a posted discount. If your checkout route accepts amount, someone will eventually send it a one.

The webhook is the only truth

checkout.session.completed is where an order becomes real. Three rules make it safe to run twice, which matters because Stripe retries and sometimes delivers the same event more than once:

Verify the signature first

An unsigned POST to that URL is someone else pretending to be Stripe. Reject it with a 400 before you read a single field.

Key the row on the session id

upsert on stripe_session_id, which is unique. A replay updates the row it already wrote instead of creating a second paid order.

Check before you issue

Look for an existing licence or entitlement on that order and return early if there is one. Otherwise a retry sends a second welcome email, which is how customers find out your webhook is broken.

Refunds

Enable charge.refunded on the endpoint and unwind everything the sale created: mark the order refunded, revoke the entitlement, void any affiliate commission. If your terms say a refund cancels something, something has to actually cancel it.

Tax

For a digital product sold from the EU, turn on Stripe Tax and add a registration for your own country at minimum. Two settings decide what the buyer sees:

  • tax_code: "txcd_10000000" marks the line as a general electronic service. Without it Stripe cannot pick a rate and charges nothing.
  • tax_behavior: "exclusive" means your figure is excluding tax and tax is added on top. inclusive means your figure already contains it.

Testing

Card 4242 4242 4242 4242, any future expiry, any CVC. To exercise the webhook locally, forward events rather than trying to guess the payload:

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

It prints a whsec_... for the session. Put that in .env while you test.

Something wrong or missing on this page? Tell us.