Photo by Eric Er on Unsplash

On this page

This chapter follows Products and prices in Stripe.

Stripe Checkout is the default path for many web apps: Stripe renders the payment form, handles wallets and local methods you enable in the Dashboard, and returns the customer to your site. You create a Checkout Session on the server and redirect to session.url.

Prerequisites

You need:

  • A Price id for a one-time product (from Products and prices)
  • STRIPE_SECRET_KEY and the shared stripe client
  • Optional: a Stripe Customer id if the buyer is logged in

Do not pass payment_method_types on the Session unless you are building Terminal card_present flows. Omitting it enables dynamic payment methods from Dashboard settings.

Create a Checkout Session on the server

routes/checkout.js
import { stripe } from '../stripe-client.js';
import { usersById } from '../store.js';
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:4242';
export async function createCheckoutSession(req, res) {
const { userId, priceId } = req.body;
const user = usersById.get(userId);
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${BASE_URL}/success.html?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${BASE_URL}/cancel.html`,
customer: user?.stripeCustomerId,
client_reference_id: userId,
metadata: { appUserId: userId ?? 'guest' },
});
res.json({ url: session.url });
}

client_reference_id and metadata help your webhook handler tie payment back to your user without trusting query parameters alone.

Redirect the browser to Checkout

public/index.html
<button id="buy">Buy Pro (lifetime)</button>
<script>
document.getElementById('buy').onclick = async () => {
const res = await fetch('/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'user_1',
priceId: 'price_REPLACE_ME',
}),
});
const { url } = await res.json();
window.location = url;
};
</script>

Wire Express:

server.js
import express from 'express';
import { createCheckoutSession } from './routes/checkout.js';
const app = express();
app.use(express.static('public'));
app.use(express.json());
app.post('/create-checkout-session', createCheckoutSession);
app.listen(4242);

In production, protect the POST route with authentication and validate priceId against your allowed catalog—never accept arbitrary price ids from anonymous clients.

Success and cancel pages

Cancel — Show a friendly message; no payment occurred.

Success — You may retrieve the Session for display:

routes/session-status.js
import { stripe } from '../stripe-client.js';
export async function getSessionStatus(req, res) {
const session = await stripe.checkout.sessions.retrieve(req.query.session_id);
res.json({
status: session.status,
paymentStatus: session.payment_status,
customerEmail: session.customer_details?.email,
});
}

session.status === 'complete' and payment_status === 'paid' indicate success for most card payments. Do not grant entitlements here alone—users can refresh or share the URL. Grant access when your webhook processes checkout.session.completed (and checkout.session.async_payment_succeeded when payment_status is unpaid until async methods settle).

Why fulfillment belongs in webhooks

Redirects are unreliable: closed tabs, back buttons, and mobile handoffs happen daily. Webhooks retry from Stripe’s infrastructure and carry signed proof of the event.

Step Safe for fulfillment?
Webhook checkout.session.completed Yes (with idempotent handler)
Success page retrieve Display only
Client-side session_id in URL Untrusted hint

The next payment chapter covers embedded UI; Webhooks in Stripe implements the handler.

Test the flow locally

Use test card 4242 4242 4242 4242, any future expiry, any CVC. Install the Stripe CLI and forward events while developing:

Terminal window
stripe listen --forward-to localhost:4242/webhook

Complete a Checkout payment and confirm events arrive before you rely on success-page logic.

Next: Custom payment forms with the Payment Element.

Check your understanding

Question 1 of 6

0 of 6 checked

  1. 1. Why should most Checkout Session integrations omit payment_method_types?

Search articles

Type a word to search published articles.

Open the search page