Does Stripe support product variants like size and color?

No. A Stripe Product has no variant, option, or stock fields, and Stripe's own guide says that if you sell shirts, you create a separate product for each size and color combination. Checkout sells the prices you put in the session, so the buyer picks a size on your page and you send the matching price. Four sizes in three colors is twelve Products, twelve Prices, and a table of your own that ties them together.
Verified against official documentationReviewed Send a correction (opens in a new tab)

The short answer on Flint

Flint is a payments API with the commerce system on top, and card processing runs on Stripe underneath. Its catalog has products with options and variants: one Crew tee product, a Size option and a Color option, and a variant for each combination you sell, with its own price, SKU, tax category, and stock. An order line names the variant, Flint copies its name and price onto the order, and payment fails before any money moves if that size is out of stock. Paid add-ons such as gift wrap or engraving are modifier sets with their own prices. The Flint requests are below.

What Stripe tells you to do

Stripe's catalog has two objects. A Product is a name, description, images, a tax code, and metadata. A Price is an amount and currency, and for subscriptions an interval. Stripe's modeling rule is to create a separate product for each distinct option a customer chooses between, and to use several prices on one product only for billing differences such as monthly versus annual or another currency. Its e-commerce example is a product for each size and color combination of a shirt.

The reason is what the buyer sees. Checkout, Payment Links, and invoices print the Product name on each line, so line items that share a product share its name, and an XL and an M bought together would read as the same item with two amounts.

The workaround, in code

Create one Product per combination and keep your own table that says which Product is the sand XL of the crew tee. Stripe has nowhere to store that grouping except metadata on each Product:

JavaScript
const sizes = ["S", "M", "L", "XL"];
const colors = ["Black", "Sand", "Navy"];

for (const color of colors) {
  for (const size of sizes) {
    const product = await stripe.products.create({
      name: `Crew tee, ${color}, ${size}`,
      metadata: { style: "crew-tee", color, size },
      default_price_data: {
        currency: "usd",
        unit_amount: size === "XL" ? 3200 : 2800,
      },
    });

    // Stripe has nowhere to group these 12 products, so you keep the map
    await db.variants.insert({
      style: "crew-tee",
      color,
      size,
      productId: product.id,
      priceId: product.default_price,
      stock: 0, // and the count, see the stock trap below
    });
  }
}

Then build the size and color picker on your product page. When the buyer checks out, look up the combination in your table, check your own stock count, and create the session with that one price:

JavaScript
// Your product page posted the buyer's size and color
const variant = await db.variants.findOne({
  style: "crew-tee",
  color: req.body.color,
  size: req.body.size,
});
if (!variant || variant.stock < 1) return res.status(409).send("Sold out");

const session = await stripe.checkout.sessions.create({
  mode: "payment",
  line_items: [{ price: variant.priceId, quantity: 1 }],
  metadata: { variant_key: `crew-tee/${variant.color}/${variant.size}` },
  success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
});

That is the whole integration surface Stripe gives you for variants. Everything that treats the twelve Products as one shirt is code you write and keep in sync: the mapping table, the picker, the stock counts, coupon product lists, and price changes. Third-party tools sell variant pickers for Stripe pages for the same reason; Checkout Page's guide, for one, lists four workarounds and names its own product as one.

Where the workaround costs you

  1. A sale on one shirt has to list every combination#

    "20% off tees" worked for black but not for sand, because the sand products were added after the coupon.

    A coupon's applies_to.products is an array of Product IDs. When each size and color is its own Product, a sale on one style lists every combination, and a color you add next month is not covered until you create a new coupon with it.

    Confirm it

    Compare the coupon's applies_to.products against the products in your mapping table for that style.

    Fix

    Generate the product list from your mapping table when you create the coupon, and create a new coupon when the style gains a combination. Coupon settings other than the name can't be edited after creation.

  2. One price change is a new Price for every combination#

    The tee went from $28 to $30 and two sizes still sell at $28.

    Stripe Prices are immutable records. To change a price you create a new Price and archive the old one, so a style in four sizes and three colors needs twelve new Prices, twelve archives, and an update to every place that stores the old IDs: your mapping table, Payment Links, and saved carts.

    Confirm it

    List active Prices for the style's products and look for more than one amount.

    Fix

    Script the change: create the new Prices, update your mapping in the same job, then archive the old Prices only after nothing points at them.

  3. Stock for each size is yours to count#

    The last medium sold twice during a restock email.

    A Stripe Product has no stock, quantity, or SKU field, so a count per size and color lives in your database, updated from webhooks. The race windows are the same as for any product, multiplied by the number of combinations.

    Confirm it

    Look for paid orders on a combination whose count in your database was already zero when the payment started.

    Fix

    Hold stock before the buyer pays, not after the webhook arrives. The Stripe inventory tracking lookup, linked below, covers the hold, the session expiry, and the five race windows.

The stock trap has its own lookup: Stripe inventory management: how to prevent overselling.

On Flint, variants are part of the catalog

One request creates the product, its options, and its variants. Each variant selects one value for each option, carries its own price and SKU, and here gets an inventory item so its stock is tracked:

cURL
curl -X POST https://api.withflintpay.com/v1/products \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: product-crew-tee" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Crew tee",
    "product_type": "physical",
    "options": [
      {
        "name": "Size",
        "values": [
          { "value": "M" },
          { "value": "XL" }
        ]
      },
      {
        "name": "Color",
        "values": [
          { "value": "Black" },
          { "value": "Sand" }
        ]
      }
    ],
    "variants": [
      {
        "name": "M / Black",
        "sku": "TEE-M-BLK",
        "unit_price_money": { "amount": 2800, "currency": "USD" },
        "line_item_tax_category": "clothing",
        "inventory_item": { "name": "Crew tee, M, Black" },
        "selected_options": [
          { "option_name": "Size", "value": "M" },
          { "option_name": "Color", "value": "Black" }
        ]
      },
      {
        "name": "M / Sand",
        "sku": "TEE-M-SND",
        "unit_price_money": { "amount": 2800, "currency": "USD" },
        "line_item_tax_category": "clothing",
        "inventory_item": { "name": "Crew tee, M, Sand" },
        "selected_options": [
          { "option_name": "Size", "value": "M" },
          { "option_name": "Color", "value": "Sand" }
        ]
      },
      {
        "name": "XL / Black",
        "sku": "TEE-XL-BLK",
        "unit_price_money": { "amount": 3200, "currency": "USD" },
        "line_item_tax_category": "clothing",
        "inventory_item": { "name": "Crew tee, XL, Black" },
        "selected_options": [
          { "option_name": "Size", "value": "XL" },
          { "option_name": "Color", "value": "Black" }
        ]
      },
      {
        "name": "XL / Sand",
        "sku": "TEE-XL-SND",
        "unit_price_money": { "amount": 3200, "currency": "USD" },
        "line_item_tax_category": "clothing",
        "inventory_item": { "name": "Crew tee, XL, Sand" },
        "selected_options": [
          { "option_name": "Size", "value": "XL" },
          { "option_name": "Color", "value": "Sand" }
        ]
      }
    ]
  }'

Flint never generates variants from options, so you still list each combination you sell, the way you would in a spreadsheet. The difference is that they belong to one product. Two variants with the same combination are rejected with VARIANT_OPTION_COMBINATION_ALREADY_EXISTS, and adding a size later is one write to the product's options and one new variant. List the variants to render your picker:

cURL
curl https://api.withflintpay.com/v1/products/prod_1kmn0aExample/variants \
  -H "Authorization: Bearer YOUR_API_KEY"

Each one says which option values it is, with no mapping table on your side (abridged):

JSON
{
  "variant_id": "var_1kmn0aExample",
  "product_id": "prod_1kmn0aExample",
  "name": "XL / Sand",
  "sku": "TEE-XL-SND",
  "unit_price_money": { "amount": 3200, "currency": "USD" },
  "selected_options": [
    { "option_name": "Size", "value": "XL" },
    { "option_name": "Color", "value": "Sand" }
  ],
  "inventory_tracking": "tracked",
  "inventory_item_id": "invi_1kmn0aExample",
  "available_for_sale": true
}

When the buyer picks XL in sand, create the order with that variant. Flint copies the name, price, SKU, and tax settings from the catalog onto the line, so your code never sends a price:

cURL
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: order-tee-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "line_items": [{ "variant_id": "var_1kmn0aExample", "quantity": 1 }],
    "inventory_routing_source": { "type": "fixed_location", "location_id": "loc_1kmn0aExample" }
  }'

Paying the order holds that variant's stock, commits it when the payment succeeds, and releases it if the payment fails. If the sand XL is gone, payment fails with INVENTORY_UNAVAILABLE before the card is charged. Send the order to hosted checkout with a checkout session, or put a variant on a payment link. The size and color picker stays on your product page; the hosted pages sell the variant it chose.

Promotions target the product, so one rule covers every size and color, including ones you add later:

JSON
{
  "name": "Tee week",
  "display_name": "20% off the crew tee",
  "redemption_type": "automatic",
  "discount_class": "line_item",
  "application_method": {
    "type": "percent_off",
    "percent_off": 20,
    "discounted_item_rules": [
      { "attribute": "line_item.product_id", "operator": "eq", "values": ["prod_1kmn0aExample"] }
    ]
  }
}

Choices that change the price without being separate stock, such as gift wrap, a monogram, or oat milk, are a modifier set attached to the product. Each choice has a price change, Flint enforces how many the buyer must pick, and the order line lists them. The catalog guide builds a full example with sizes, modifiers, and a bundle.

Who Flint is built for

US businesses selling online or in their own app, paid in USD, that want the catalog, stock, orders, checkout, promotions, and automatic US sales tax behind one API instead of a payment API plus the tables above. See Flint for online stores.

Stripe and Flint catalogs, side by side

Stripe Products and PricesFlint catalog
One shirt in 4 sizes and 3 colors12 Products and 12 Prices1 product, 2 options, 12 variants
Which combination a record isMetadata and your mapping tableselected_options on the variant
Price per sizeA Price per Productunit_price_money per variant
SKUMetadatasku, unique across the catalog
Stock per sizeYour database and webhooksHeld at payment, per variant and location
Discount one styleCoupon applies_to.products listing every combinationOne rule on line_item.product_id or a category
Paid add-onsOptional items, up to 10, as separate productsModifier sets with price changes
Where the buyer picks a sizeYour page, before you create the sessionYour page, before you create the order

Questions, answered

Does Stripe support product variants?

No. A Stripe Product has no variant, option, or attribute fields. Stripe's own guidance is to create a separate product for each size and color combination. Checkout sells the prices you pass in the session, so the buyer chooses a size on your page before Checkout opens.

Should I use one Stripe product with several prices for sizes?

Stripe recommends against it when the options are different things the buyer chooses between. Line items that share a product share its name and description, so the receipt and invoice don't say which size was bought. Stripe reserves multiple prices on one product for billing differences such as monthly versus annual or another currency.

How do I let buyers pick a size and color with Stripe Checkout?

Build the picker on your product page, map the chosen size and color to a Price ID in your own table, and create the Checkout Session with that price. A dropdown custom field can ask for a size, but it doesn't change the price or the product being sold.

Can Stripe track inventory for each variant?

No. Products have no stock field. You keep a count per combination in your database and update it from webhooks, holding stock while a Checkout Session is open.

Which payments API has product variants built in?

Flint's catalog has products with options, such as Size and Color, and variants, each with its own price, SKU, tax category, and optional stock tracking. An order line names a variant, and Flint copies its name and price onto the order and holds its stock when the order is paid. Card processing runs on Stripe underneath.

Is Flint a good alternative to Stripe for selling products with sizes and colors?

Yes, if you sell online or in an app to buyers paying in USD from a US business. Flint keeps the catalog, variants, stock, orders, and checkout behind one API, so you don't maintain a mapping table, a stock counter, or price fan-out scripts.

Sources

All sources accessed October 15, 2026.