How to build a shopping cart with Stripe

Stripe has no cart object. A Payment Link sells a fixed list of up to 20 items, and a Checkout Session takes the line items you pass when you create it: in payment mode, up to 100 line items and optional items combined, at most 10 of them optional. A cart where buyers add and remove products as they browse lives in your app. You store it, price it on your server, and turn it into a Checkout Session when the buyer clicks checkout.
Verified against official documentationReviewed Send a correction (opens in a new tab)

The short answer on Flint

On Flint the order is the cart. The first add-to-cart creates an order from your catalog variants; later adds, quantity changes, and removals update it, and promotion codes stack by your rules. Flint prices every change on the server and returns the totals. When the buyer checks out, one checkout session for that order collects the address, offers shipping or pickup, adds US sales tax, holds stock while the buyer pays, and takes the payment. The paid order is what you fulfill. Card processing runs on Stripe underneath. The Flint requests are below.

What Stripe gives a store with several products

  • Payment Links take up to 20 line items with flat-rate prices, fixed when you create the link. Buyers can change quantities if you allow it and add optional items you set up, but they can't browse and build a cart on the link.
  • Checkout Sessions take up to 100 line items and optional items combined in payment mode, with Stripe recommending you consolidate past a few dozen, at most 10 of them optional, one coupon or promotion code, and up to 5 shipping rates. On the hosted page, shipping rates can't change with the buyer's address.
  • After payment, Stripe's fulfillment guide has you retrieve the session's line items, save them to your own database, reconcile quantities the buyer adjusted, and update your stock records.

Everything between the first add-to-cart and the Checkout Session, the cart itself, is yours to build.

The workaround: a server cart and a Checkout Session

Keep the cart on your server, keyed to the browser session, and build the session from it when the buyer checks out:

JavaScript
// The browser sends IDs and quantities. Never trust its prices.
app.post("/cart/checkout", async (req, res) => {
  const cart = await db.carts.get(req.session.cartId);   // your cart table
  const line_items = cart.items.map((item) => ({
    price: catalog.priceIdFor(item.productId, item.size), // your mapping
    quantity: item.quantity,
  }));

  // One coupon or promotion code per session: merge yours first
  const discounts = cart.couponId ? [{ coupon: cart.couponId }] : [];

  // Up to 5 fixed shipping rates; the hosted page can't change them by address
  const shipping_options = shippingRatesFor(cart).slice(0, 5);

  await inventory.hold(cart.items, { minutes: 31 }); // your stock holds
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items,
    discounts,
    shipping_options,
    shipping_address_collection: { allowed_countries: ["US"] },
    automatic_tax: { enabled: true },
    expires_at: Math.floor(Date.now() / 1000) + 31 * 60, // just past Stripe's 30-minute minimum
    client_reference_id: cart.id,
    success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
  });
  res.redirect(303, session.url);
});

Behind those calls sit a cart table, a product-to-Price mapping, your discount rules, a shipping table, stock holds, and the webhook that writes the order. These are the places it goes wrong:

Where a hand-built Stripe cart goes wrong

  1. The cart total comes from the browser#

    An order went through at $1.00 for a $100 item.

    A cart kept in localStorage or a cookie holds whatever the browser says. If your server builds line items with price_data from cart amounts instead of looking up Price IDs, the buyer sets the price.

    Confirm it

    Compare amount_subtotal on recent sessions with your catalog prices for the same items.

    Fix

    Send only product or variant IDs and quantities from the browser, and build every line item on the server from Price IDs you control.

  2. The cart shows two discounts and Checkout takes one#

    A free-shipping offer and a 10% code both showed in the cart, and Checkout applied only the code.

    A Checkout Session accepts one coupon or promotion code. Discounts your cart page promised have to be merged into one coupon, or priced into the line items, before the session is created.

    Confirm it

    Look for sessions where your cart's discount total differs from total_details.amount_discount.

    Fix

    Run your own discount engine on the server and pass one combined coupon, as /reference/stripe-checkout-multiple-coupons shows, or limit the cart to one code.

  3. Shipping can't change with the address on the hosted page#

    Alaska orders paid the same flat rate as local ones.

    A Checkout Session takes up to five shipping options, and Stripe's docs say the hosted page doesn't support customizing them from the buyer's address. That needs the embedded form or Elements.

    Confirm it

    List sessions by shipping state and compare the rate paid with your carrier cost.

    Fix

    Ask for the address in your cart and pass the matching rates, or move checkout to the embedded form and update options on address change.

  4. The cart, the session, and your order disagree#

    Fulfillment shipped two mugs; the session says the buyer changed it to one.

    The cart lives in your app, the session holds its own copy of the line items, and after payment Stripe's fulfillment guide has you save the line items to your database and reconcile quantities the buyer adjusted. Three copies of one order can drift.

    Confirm it

    Compare what you fulfilled with the session's line_items for a sample of orders with adjustable quantities.

    Fix

    Treat the session's line items as the record of what was paid, write your order from them in the webhook, and ignore the cart after the session is created.

  5. Two carts buy the last one#

    Two buyers paid for the last item in stock.

    Stripe products have no stock count, and a Checkout Session can stay open for up to 24 hours. Holding stock while a cart pays, and releasing it when the session expires, is your inventory code.

    Confirm it

    Search for paid orders that took a stock count below zero.

    Fix

    Hold stock when you create the session and release it on checkout.session.expired; /reference/stripe-inventory-tracking walks through the race windows.

On Flint, the order is the cart

Create the order when the buyer adds the first item. Keep its ID with the browser session:

Shell
# The first "Add to cart" creates the order
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: cart-8f2c-create" \
  -H "Content-Type: application/json" \
  -d '{
    "line_items": [
      { "variant_id": "var_1kmn0aExample", "quantity": 1 }
    ]
  }'

Each later change is one call, and each returns the whole order with fresh pricing_amounts to render in the cart:

Shell
# Later adds append lines; Flint prices them from your catalog
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/line-items \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: cart-8f2c-add-2" \
  -H "Content-Type: application/json" \
  -d '{ "line_items": [{ "variant_id": "var_2bqr7dExample", "quantity": 2 }] }'

# Change a quantity, or DELETE the same route to remove the line
curl -X PATCH https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/line-items/li_2bqr7dExample \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "quantity": 3 }'

A promotion code is a request too. Flint checks the code's rules and how it combines with any automatic offers, applies what qualifies, and tells you in a warning when a code can't combine:

cURL
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/discounts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "promotion": { "promotion_code": "SPRING10" } }'

When the buyer clicks checkout, create a session for the order and redirect to its url:

cURL
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: cart-8f2c-checkout" \
  -H "Content-Type: application/json" \
  -d '{ "order_id": "ord_1kmn0aExample" }'

What the hosted checkout does with it:

  • Delivery. Collects the address and offers your shipping methods, priced by region, free over a threshold, or from your own rate callback, plus in-store pickup and local delivery. See Delivery options.
  • Tax. With automatic tax on, adds US sales tax once the address is in, on the total after discounts. See Sales tax.
  • Stock. Holds tracked stock when payment starts, commits it when payment succeeds, and releases it if payment fails. See Inventory.
  • The record. order.paid fires on the same order, with the items, discounts, shipping, and tax already on it, so there is no session to copy into an order table.

If the buyer goes back to the cart after you created the session, change the order as before. A change you make with your API key ends the open session, so its URL can't collect the old total; create a new session when they check out again. To render the checkout on your own domain instead, create the session with "surface": "embedded", as Build your own checkout describes.

Who Flint is built for

US businesses selling physical or digital goods in USD on their own site or app, and the developers and agencies building those stores. See the Flint commerce API, and, if you use a hosted cart service on top of Stripe today, Flint as a Snipcart alternative.

A Stripe cart and a Flint order, side by side

Stripe Checkout and Payment LinksFlint orders and checkout
Cart objectNone; your tableThe order
ItemsUp to 20 on a Payment Link; up to 100 on a sessionCatalog variants and bundles added to the order
Prices and totalsYour server, then the sessionFlint, on every change, in pricing_amounts
Discount codesOne coupon or promotion code per sessionCodes and automatic offers, stacked by rule
Shipping by address on the hosted pageUp to 5 fixed ratesRegion rates, thresholds, pickup, local delivery, callbacks
Stock during paymentYour holdsHeld when payment starts
Record you fulfill fromYour copy of the session's line itemsThe paid order, on order.paid

Questions, answered

Does Stripe have a shopping cart?

No cart object. A Payment Link sells a fixed list of up to 20 items, and a Checkout Session takes the line items you pass when you create it, up to 100 line items and optional items combined in payment mode, at most 10 of them optional. A cart where buyers add and remove products while they browse is something you build and store, then turn into a Checkout Session.

Can a Stripe Payment Link have multiple products?

Yes, up to 20 line items with flat-rate prices, chosen when you create the link. The buyer can change quantities if you allow it and add optional items you set up, but can't browse your catalog and build their own cart on the link.

How do I check out multiple products with Stripe Checkout?

Keep the cart on your server as product IDs and quantities, build line_items from your Price IDs when the buyer clicks checkout, create the session, and redirect to its URL. After payment, read the session's line items in your webhook and save the order to your own database.

What is the best shopping cart API to use with Stripe processing?

Flint Pay, for US businesses selling in USD. A Flint order is the cart: you add and remove catalog items, apply promotion codes that stack by rule, and Flint computes discounts, shipping, and US sales tax on the server. One checkout session collects payment on Stripe's processing, holds stock while the buyer pays, and the paid order is the record you fulfill from.

Do I need Snipcart or another cart service on top of Stripe?

Not with Flint. Snipcart's pricing page lists 2% per transaction plus payment gateway fees, or $20 a month under $1,000 in sales. Flint's orders, catalog, promotions, shipping, inventory, and checkout are included in its published card rate of 3.79% + 35¢, which covers Stripe's processing fees, with automatic tax at 0.50% on taxed payments and no monthly fee.

Is Flint a good alternative to building a cart on Stripe?

Yes, for US businesses selling physical or digital goods online in USD that would otherwise build a cart, discounts, shipping rules, tax, stock holds, and an order table around Checkout. Flint's API, Node and PHP SDKs, and hosted checkout cover those pieces.

Sources

All sources accessed October 15, 2026.