Learn how to dynamically add, remove, or update line items included in a Checkout Session.
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.
- Update tax rates: If you’re not using Stripe Tax, you can dynamically update tax rates on line items based on the shipping address entered.
Stripe の公式ライブラリを使用して、アプリケーションから Stripe API にアクセスします。
gem install stripe -v 15.1.0-beta.2
このベータ版を使用するには、まず checkout_server_update_beta=v1
のベータ版ヘッダーを使用するように SDK を更新します。
Stripe.api_key = 'sk_test_BQokikJOvBiI2HlWgH4olfQ2'
Stripe.api_version = '2025-03-31.basil; checkout_server_update_beta=v1;'
Checkout セッションの作成時に permissions.update_line_items=server_only を渡して、サーバーから項目を更新できるようにします。このオプションを渡すと、updateLineItemQuantity などの項目に対するクライアント側の更新も無効になり、すべての更新がサーバーを経由するようになります。
curl https://api.stripe.com/v1/checkout/sessions \
-u "sk_test_BQokikJOvBiI2HlWgH4olfQ2
:" \
-H "Stripe-Version: 2025-03-31.basil; checkout_server_update_beta=v1;" \
-d ui_mode=custom \
-d "permissions[update_line_items]"=server_only \
-d "line_items[0][price]"= \
-d "line_items[0][quantity]"=1 \
-d mode=subscription \
--data-urlencode return_url="https://example.com/return"
サーバー上に新しいエンドポイントを作成して、Checkout セッションのラインアイテムを更新します。これは、後の手順でフロントエンドから呼び出します。
セキュリティのヒント
クライアント側のコードは、ユーザーが完全に制御可能な環境で実行できることを確認しておかなければなりません。悪意のあるユーザーは、クライアント側の検証をバイパスしたり、リクエストを傍受して内容を変更したり、サーバーにまったく新しいリクエストを送信したりする可能性があります。
エンドポイントを設計する際は、以下を考慮することをお勧めします。
- エンドポイントは過度に汎用的にするのではなく、特定の顧客とのやり取りに対応するように設計します (たとえば、一般的な「更新」アクションではなく「クロスセル商品の追加」アクションなど)。特定のエンドポイントは目的がわかりやすく、検証ロジックの記述と維持が簡単になります。
- セッションデータをクライアントからエンドポイントに直接渡さないようにしてください。悪意のあるクライアントがリクエストデータを変更する可能性があり、Checkout セッションの状態を判断するためのソースとしての信頼性が低くなってしまいます。代わりに、セッション ID をサーバーに渡し、それを使用して Stripe の API からデータを安全に取得することが可能です。
require 'sinatra'
require 'json'
require 'stripe'
set :port, 4242
Stripe.api_key = 'sk_test_BQokikJOvBiI2HlWgH4olfQ2'
Stripe.api_version = '2025-03-31.basil; checkout_server_update_beta=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 could not process your request. Please try again later.'
}.to_json
end
begin
new_price = interval == 'yearly' ? YEARLY_PRICE_ID : MONTHLY_PRICE_ID
line_items = [{
price: new_price,
quantity: 1,
}]
Stripe::Checkout::Session.update(checkout_session_id, {
line_items: line_items,
})
{ type: 'success' }.to_json
rescue Stripe::StripeError
status 400
{
type: 'error',
message: 'We couldn't process your request. Please try again later.'
}.to_json
rescue StandardError
status 500
{
type: 'error',
message: 'Something went wrong on our end. Please try again later.'
}.to_json
end
end
ラインアイテムを更新する場合は、ラインアイテムの配列全体を再送信する必要があります。
- 既存のラインアイテムを保持するには、そのラインアイテムの
id
を指定します。 - 既存のラインアイテムを更新するには、そのラインアイテムの
id
と、更新するフィールドの新しい値を指定します。 - 新しいラインアイテムを追加するには、
id
を付けずに price
と quantity
を指定します。 - 既存のラインアイテムを削除するには、再送信された配列からそのラインアイテムの ID を除外します。
- ラインアイテムを並べ替えるには、再送信された配列内の目的の位置にラインアイテムの
id
を指定します。
custom_checkout_server_updates_1
ベータヘッダーを使用して stripe.js を初期化します。
const stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx'
, {
betas: ['custom_checkout_server_updates_1'],
});
フロントエンドから更新をトリガーするには、サーバーにリクエストを送信し、それを runServerUpdate でラップします。リクエストが成功すると、Session オブジェクトが新しい項目で更新されます。
<button id="change-subscription-interval" role="switch" aria-checked="false">
Save with a yearly subscription
</button>
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: checkout.session().id,
interval: isCurrentSubscriptionMonthly ? "yearly" : "monthly",
})
});
};
const response = await checkout.runServerUpdate(updateCheckout);
if (!response.ok) {
return;
}
const isNewSubscriptionMonthly = !isCurrentSubscriptionMonthly;
button.setAttribute("aria-checked", !isNewSubscriptionMonthly);
button.textContent = isNewSubscriptionMonthly
? "Save with a yearly subscription"
: "Use monthly subscription";
});