Photo by Yosuke Ota on Unsplash

On this page

This chapter follows Customers in Stripe.

Checkout Sessions and subscriptions reference Price ids (price_...), not free-form amounts in the client. That keeps your catalog auditable and stops tampered browsers from changing what someone pays.

Product versus Price

A Product is the thing you sell—name, description, images, tax category. A Price attaches billing terms: unit amount, currency, and whether billing is one-time or recurring.

One Product can have many Prices (monthly vs annual, USD vs EUR, legacy vs current rate).

scripts/seed-catalog.js
import { stripe } from '../stripe-client.js';
const product = await stripe.products.create({
name: 'Pro plan',
description: 'Full access to the app',
});
const monthly = await stripe.prices.create({
product: product.id,
unit_amount: 2000,
currency: 'usd',
recurring: { interval: 'month' },
lookup_key: 'pro_monthly_usd',
});
const lifetime = await stripe.prices.create({
product: product.id,
unit_amount: 19900,
currency: 'usd',
lookup_key: 'pro_lifetime_usd',
});
console.log({ productId: product.id, monthly: monthly.id, lifetime: lifetime.id });

Run seed scripts against test mode first. Production catalog changes usually go through review and Dashboard or infrastructure-as-code workflows.

One-time and recurring prices

Field One-time Recurring
recurring Omitted { interval: 'month' | 'year' | ... }
Used in Checkout mode payment subscription
Invoices Created per Checkout payment Generated each cycle on Subscription

Trials and usage-based components attach at subscription or Price level; see Subscriptions in Stripe.

Why prices are immutable

You cannot change unit_amount or currency on an existing Price. Stripe treats a Price as a historical record—active subscriptions must keep billing the terms they started on.

To raise prices:

  1. Create a new Price on the same (or new) Product.
  2. Point new Checkout Sessions at the new Price id.
  3. Migrate existing subscribers with proration rules (subscription update APIs or Customer Portal).

Deactivate old Prices with active: false so they no longer appear in Dashboard pickers but existing subscriptions keep working.

Lookup keys and catalog resolution

lookup_key is a stable string you choose (for example pro_monthly_usd). At runtime, resolve it to the current Price id so deploys do not hard-code price_... strings:

const prices = await stripe.prices.list({
lookup_keys: ['pro_monthly_usd'],
active: true,
limit: 1,
});
const priceId = prices.data[0]?.id;

Keep lookup keys unique per mode. Document which key each environment uses in config, not in the client.

Dashboard, API, and tax behavior

Many teams create Products in the Dashboard during early development, then move to seed scripts or Stripe’s product export/import for staging parity.

Tax behavior on a Price (exclusive, inclusive, or unspecified) interacts with Stripe Tax. Enabling automatic tax without registrations collects nothing while looking enabled—a common misconfiguration. Configure registrations before turning on automatic_tax in Checkout.

Archiving and displaying catalog in your app

List active Products with expanded default prices for a pricing page:

const products = await stripe.products.list({ active: true, limit: 20 });
const prices = await stripe.prices.list({ active: true, product: products.data[0].id });

Your marketing site can cache this JSON; payment flows should still verify Price ids on the server when creating Checkout Sessions.

Next: Accepting payments with Stripe Checkout.

Check your understanding

Question 1 of 6

0 of 6 checked

  1. 1. What does a Checkout Session reference for line items in a typical catalog integration?

Search articles

Type a word to search published articles.

Open the search page