# Dynamically update line items Update line items in response to changes made during checkout. # Full hosted page Dynamically add, remove, or update line items included in a [Checkout Session](https://docs.stripe.com/api/checkout/sessions/object.md). This allows you to: - **Check inventory**: Run inventory checks and holds when customers attempt to change item quantities. - **Add new products**: Add a complimentary product if the order total exceeds a specific amount. - **Update shipping rates**: If the order total changes, update shipping rates by combining the method described in this guide with our guide on how to [Customize shipping options during checkout](https://docs.stripe.com/payments/checkout/custom-shipping-options.md). - **Update tax rates**: If you’re not using [Stripe Tax](https://docs.stripe.com/tax/checkout.md), you can dynamically update [tax rates](https://docs.stripe.com/billing/taxes/collect-taxes.md?tax-calculation=tax-rates#adding-tax-rates-to-checkout) on line items based on the shipping address entered. Dynamic updates aren’t supported for hosted pages. You can use this feature with [Elements with the Checkout Sessions API](https://docs.stripe.com/payments/advanced/dynamically-update-line-items.md). # Full embedded page Dynamically add, remove, or update line items included in a [Checkout Session](https://docs.stripe.com/api/checkout/sessions/object.md). This allows you to: - **Check inventory**: Run inventory checks and holds when customers attempt to change item quantities. - **Add new products**: Add a complimentary product if the order total exceeds a specific amount. - **Update shipping rates**: If the order total changes, update shipping rates by combining the method described in this guide with our guide on how to [Customize shipping options during checkout](https://docs.stripe.com/payments/checkout/custom-shipping-options.md). - **Update tax rates**: If you’re not using [Stripe Tax](https://docs.stripe.com/tax/checkout.md), you can dynamically update [tax rates](https://docs.stripe.com/billing/taxes/collect-taxes.md?tax-calculation=tax-rates#adding-tax-rates-to-checkout) on line items based on the shipping address entered. We’re working on adding support for this feature when using an embedded form. to be notified when this is released or use this feature with [Elements with the Checkout Sessions API](https://docs.stripe.com/payments/advanced/dynamically-update-line-items.md) instead. # Embedded form > Learn more about [the embedded form integration](https://docs.stripe.com/payments/checkout/how-checkout-works.md?payment-ui=checkout-form). Learn how to dynamically add, remove, or update line items included in a [Checkout Session](https://docs.stripe.com/api/checkout/sessions/object.md). ### Use cases This guide demonstrates how to update line items to upsell a subscription, but you can also: - **Check inventory**: Run inventory checks and holds when customers attempt to change item quantities. - **Add new products**: Add a complimentary product if the order total exceeds a specific amount. - **Update shipping rates**: If the order total changes, update shipping rates by combining the method described in this guide with what’s out on [Customize shipping options during checkout](https://docs.stripe.com/payments/checkout/custom-shipping-options.md). - **Update tax rates**: If you’re not using [Stripe Tax](https://docs.stripe.com/tax/checkout.md), you can dynamically update [tax rates](https://docs.stripe.com/billing/taxes/collect-taxes.md?tax-calculation=tax-rates#adding-tax-rates-to-checkout) on line items based on the shipping address entered. ## Create a Checkout Session [Server-side] From your server, create a *Checkout Session* (A Checkout Session represents your customer's session as they pay for one-time purchases or subscriptions through Checkout. After a successful payment, the Checkout Session contains a reference to the Customer, and either the successful PaymentIntent or an active Subscription). > To use this feature, ensure your SDK version is `2025-03-31.basil` or later. ```curl curl https://api.stripe.com/v1/checkout/sessions \ -u "<>:" \ -H "Stripe-Version: 2026-05-27.preview; custom_checkout_payment_form_preview=v1" \ -d ui_mode=form \ -d mode=subscription \ -d "line_items[0][price]={{PRICE_ID}}" \ -d "line_items[0][quantity]=1" \ --data-urlencode "return_url=https://example.com/return" ``` ## Dynamically update line items [Server-side] Create an endpoint on your server to update the line items on the Checkout Session. You’ll call this from the front end in a later step. > Client-side code runs in an environment that’s controlled by the user. A malicious user can bypass your client-side validation, intercept and modify requests, or create new requests to your server. When creating an endpoint, we recommend the following: - Create endpoints for specific customer interactions instead of making them generic. For example, “add cross-sell items” instead of a general “update” action. Specific endpoints can help with writing and maintaining validation logic. - Don’t pass [session data](https://docs.stripe.com/js/custom_checkout/session_object) directly from the client to your endpoint. Malicious clients can modify request data, making it an unreliable source for determining the Checkout Session state. Instead, pass the [session ID](https://docs.stripe.com/js/custom_checkout/session_object#custom_checkout_session_object-id) to your server and use it to securely retrieve the data from the Stripe API. #### Ruby ```ruby require 'sinatra' require 'json' require 'stripe' set :port, 4242 # Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. # Find your keys at https://dashboard.stripe.com/apikeys. client = Stripe::StripeClient.new( '<>', stripe_version: '2026-05-27.dahlia; custom_checkout_payment_form_preview=v1;', ) MONTHLY_PRICE_ID = '{{MONTHLY_PRICE}}' YEARLY_PRICE_ID = '{{YEARLY_PRICE}}' post '/change-subscription-interval' do content_type :json request.body.rewind request_data = JSON.parse(request.body.read) checkout_session_id = request_data['checkout_session_id'] interval = request_data['interval'] if checkout_session_id.nil? || !['yearly', 'monthly'].include?(interval) status 400 return { type: 'error', message: "We couldn't process your request. Please try again later." }.to_json end begin # 1. Create the new line items for the Checkout Session. new_price = interval == 'yearly' ? YEARLY_PRICE_ID : MONTHLY_PRICE_ID line_items = [{ price: new_price, quantity: 1, }] # 2. Update the Checkout Session with the new line items. client.v1.checkout.sessions.update(checkout_session_id, { line_items: line_items, }) # 3. Return a success response. { type: 'success' }.to_json rescue Stripe::StripeError # Handle Stripe errors with a generic error message status 400 { type: 'error', message: "We couldn't process your request. Please try again later." }.to_json rescue StandardError # Handle unexpected errors status 500 { type: 'error', message: 'Something went wrong on our end. Please try again later.' }.to_json end end ``` When updating line items, you must retransmit the entire array of line items. - To keep an existing line item, specify its `id`. - To update an existing line item, specify its `id` along with the new values of the fields to update. - To add a new line item, specify a `price` and `quantity` without an `id`. - To remove an existing line item, omit the line item’s ID from the retransmitted array. - To reorder a line item, specify its `id` at the desired position in the retransmitted array. ## Update the client SDK [Client-side] #### HTML + JS Initialize Stripe.js. ```javascript const stripe = Stripe('<>', { betas: ['custom_checkout_payment_form_1'] }); ``` #### React Initialize the `stripe` instance. ```javascript import {loadStripe} from '@stripe/stripe-js'; const stripe = loadStripe("<>", { betas: ['custom_checkout_payment_form_1'] }); ``` ## Request server updates [Client-side] #### HTML + JS From your front end, create a function to send an update request to your server and wrap it in [runServerUpdate](https://docs.stripe.com/js/custom_checkout/run_server_update). A successful request updates the [Session](https://docs.stripe.com/js/custom_checkout/session_object) object with the new line items. `runServerUpdate` enforces a 20-second timeout for your update function. If your function doesn’t resolve within 20 seconds, `runServerUpdate` returns an error. Wrap `runServerUpdate` calls in `try`/`catch` blocks to handle any errors, and record metrics to diagnose timeouts and other failures. Wrap `runServerUpdate` calls in `try`/`catch` blocks to handle errors from your server and from `runServerUpdate` itself (for example, timeouts). `response.type === 'error'` covers failures in Stripe’s internal retrieval of the updated session. Errors returned by your own server (such as 4xx or 5xx responses) aren’t reflected in `response` because the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) resolves for any completed HTTP response regardless of status code. Check `response.ok` inside your update function and throw on failure so that the `catch` block is reached. ```html ``` ```javascript document.getElementById('change-subscription-interval') .addEventListener("click", async (event) => { const button = event.target; const isCurrentSubscriptionMonthly = button.getAttribute("aria-checked") === "false"; const updateCheckout = async () => { const response = await fetch("/change-subscription-interval", { method: "POST", headers: { "Content-type": "application/json", }, body: JSON.stringify({ checkout_session_id: actions.getSession().id, interval: isCurrentSubscriptionMonthly ? "yearly" : "monthly", }) }); if (!response.ok) { const body = await response.json(); throw new Error(body.message); } }; try {const response = await checkout.runServerUpdate(updateCheckout); if (response.type === 'error') { // Handle Stripe API errors (for example, session retrieval failure) return; } } catch (error) { // Handle promise rejection from your server (4xx/5xx errors) or // from runServerUpdate itself (for example, timeouts). // error.message contains the message thrown from your update function. return; } // Update toggle state on success const isNewSubscriptionMonthly = !isCurrentSubscriptionMonthly; button.setAttribute("aria-checked", !isNewSubscriptionMonthly); button.textContent = isNewSubscriptionMonthly ? "Save with a yearly subscription" : "Use monthly subscription"; }); ``` #### React From your front end, send an update request to your server and wrap it in [runServerUpdate](https://docs.stripe.com/js/custom_checkout/run_server_update). A successful request updates the [Session](https://docs.stripe.com/js/custom_checkout/session_object) object with the new line items. `runServerUpdate` enforces a 20-second timeout for your update function. If your function doesn’t resolve within 20 seconds, `runServerUpdate` returns an error. Wrap `runServerUpdate` calls in `try`/`catch` blocks to handle any errors, and record metrics to diagnose timeouts and other failures. Wrap `runServerUpdate` calls in `try`/`catch` blocks to handle errors from your server and from `runServerUpdate` itself (for example, timeouts). `response.type === 'error'` covers failures in Stripe’s internal retrieval of the updated session. Errors returned by your own server (such as 4xx or 5xx responses) aren’t reflected in `response` because the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) resolves for any completed HTTP response regardless of status code. Check `response.ok` inside your update function and throw on failure so that the `catch` block is reached. ```jsx import React from 'react'; import {useCheckoutElements} from '@stripe/react-stripe-js/checkout'; const ChangeSubscriptionInterval = () => { const [isSubscriptionMonthly, setIsSubscriptionMonthly] = React.useState(false); const checkoutState = useCheckoutElements(); if (checkoutState.type === 'loading') { return (
Loading...
); } else if (checkoutState.type === 'error') { return (
Error: {checkoutState.error.message}
); } let actions = null; const loadActionsResult = await checkoutState.loadActions(); if (loadActionsResult.type === 'success') { actions = loadActionsResult.actions; } else { return (
Error: {loadActionsResult.error.message}
); } const updateCheckout = async () => { const response = await fetch("/change-subscription-interval", { method: "POST", headers: { 'Content-type': 'application/json', }, body: JSON.stringify({ checkout_session_id: actions?.getSession()?.id, interval: isSubscriptionMonthly ? 'yearly' : 'monthly' }) }); if (!response.ok) { const body = await response.json(); throw new Error(body.message); } }; const handleClick = async () => { try {const response = await actions?.runServerUpdate(updateCheckout); if (response.type === 'error') { // Handle Stripe API errors (for example, session retrieval failure) return; } } catch (error) { // Handle promise rejection from your server (4xx/5xx errors) or // from runServerUpdate itself (for example, timeouts). // error.message contains the message thrown from your update function. return; } // Update toggle state on success setIsSubscriptionMonthly(!isSubscriptionMonthly); }; return ( ); }; ```