# Dynamically update line items Learn how to modify pricing and the contents of a cart during checkout. 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. > #### Payment Intents API > > If you use the Payment Intents API, you must manually track line item updates and modify the payment amount, or by creating a new PaymentIntent with adjusted amounts. ## Set up the SDK [Server-side] Use our official libraries to access the Stripe API from your application: #### Ruby ```bash gem install stripe -v 15.1.0 ``` ## Update the server SDK [Server-side] To use this feature, ensure your SDK version is `2025-03-31.basil` or later. #### Ruby ```ruby # Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. # Find your keys at https://dashboard.stripe.com/apikeys. Stripe.api_key = '<>' Stripe.api_version = '2025-03-31.basil' ``` ## Create a Checkout Session [Server-side] ```curl curl https://api.stripe.com/v1/checkout/sessions \ -u "<>:" \ -H "Stripe-Version: 2025-03-31.basil" \ -d ui_mode=elements \ -d "line_items[0][price]={{PRICE_ID}}" \ -d "line_items[0][quantity]=1" \ -d mode=subscription \ --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. Stripe.api_key = '<>' Stripe.api_version = '2025-03-31.basil' 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. Stripe::Checkout::Session.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('<>'); ``` #### React Initialize the `stripe` instance. ```javascript import {loadStripe} from '@stripe/stripe-js'; const stripe = loadStripe("<>"); ``` ## 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. ```html ``` ```javascript document.getElementById('change-subscription-interval') .addEventListener("click", async (event) => { const button = event.target; const isCurrentSubscriptionMonthly = button.getAttribute("aria-checked") === "false"; const updateCheckout = () => { return fetch("/change-subscription-interval", { method: "POST", headers: { "Content-type": "application/json", }, body: JSON.stringify({ checkout_session_id: actions.getSession().id, interval: isCurrentSubscriptionMonthly ? "yearly" : "monthly", }) }); }; try {const response = await checkout.runServerUpdate(updateCheckout); if (!response.ok) { // Handle error state return; } } catch (error) { // Handle promise rejection (for example, timeouts) 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. ```jsx import React from 'react'; import {useCheckout} from '@stripe/react-stripe-js/checkout'; const ChangeSubscriptionInterval = () => { const [isSubscriptionMonthly, setIsSubscriptionMonthly] = React.useState(false); const checkoutState = useCheckout(); if (checkoutState.type === 'loading') { return (
Loading...
); } else if (checkoutState.type === 'error') { return (
Error: {checkoutState.error.message}
); } const {runServerUpdate, id} = checkoutState.checkout; const updateCheckout = () => fetch("/change-subscription-interval", { method: "POST", headers: { 'Content-type': 'application/json', }, body: JSON.stringify({ checkout_session_id: id, interval: isSubscriptionMonthly ? 'yearly' : 'monthly' }) }); const handleClick = async () => { try {const response = await runServerUpdate(updateCheckout); if (!response.ok) { // Handle error state return; } } catch (error) { // Handle promise rejection (for example, timeouts) return; } // Update toggle state on success setIsSubscriptionMonthly(!isSubscriptionMonthly); }; return ( ); }; ```