# 決済金額を動的に更新する 決済中に顧客が選択内容を変更した場合に合計金額を変更する方法をご紹介します。 顧客が購入する商品や決済金額を変更した場合に、[Checkout Session](https://docs.stripe.com/api/checkout/sessions.md) または [Payment Intent](https://docs.stripe.com/api/payment_intents.md) の金額を更新します。サーバーで合計を再計算してから、PaymentIntent の金額を更新します。 #### 一般的なユースケース - アドオン (ギフトラップや保証など) を追加または削除します。 - 別の配送方法または配送速度を選択してください。 - サービスまたは請求を追加します。 - 割引コードまたは税引前ストアクレジットを適用または削除します。 #### セキュリティのベストプラクティス - サーバーで金額を再計算します。クライアントから提供された価格や合計を信頼しないでください。 - ビジネスルールに基づいて更新をオーソリします (最大数量の適用など)。 - 完了または期限切れになっていないアクティブな Session のみを更新します。 #### 制約と動作 - Payment Intent または Checkout Session が決済を待っている間に金額を更新できます (`requires_payment_method` や `requires_confirmation` など)。 - 確定後は、通常、金額を増やすことはできません。 # Checkout Sessions API > This is a Checkout Sessions API for when payment-ui is embedded-components. View the full page at https://docs.stripe.com/payments/advanced/dynamically-update-amounts?payment-ui=embedded-components. ## クライアント SDK を更新する [クライアント側] Checkout Sessions API で Elements を使用する場合は、サーバーへのクライアントコールを `runServerUpdate` にラップして、決済の状態と合計を更新します。 `runServerUpdate` の呼び出しを `try` / `catch` ブロックで囲み、サーバーおよび `runServerUpdate` 自体からのエラー (タイムアウトなど) を処理します。`response.type === 'error'` は、Stripe が更新済みセッションを内部で取得する際の失敗を対象とします。サーバー側で返されたエラー (4xx や 5xx レスポンスなど) は `response` に反映されません。[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) はステータスコードに関係なく、完了した HTTP レスポンスに対して解決するためです。更新関数内で `response.ok` を確認し、失敗時にスローすることで `catch` ブロックに到達させます。 #### HTML + JS ```javascript import {loadStripe} from '@stripe/stripe-js'; // Optional: include beta flags if your integration requires them const stripe = await loadStripe('<>', { betas: ['custom_checkout_server_updates_1'], }); const checkout = stripe.initCheckoutElementsSdk({ clientSecret, elementsOptions: {/* ... */}, }); // Example: Add additional service using price_data const loadActionsResult = await checkout.loadActions(); if (loadActionsResult.type === 'success') { const actions = loadActionsResult.actions; const session = actions.getSession(); document .getElementById('add-service') .addEventListener('click', async () => { const updateOnServer = async () => { const response = await fetch('/update-custom-amount', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ checkout_session_id: session.id, product_id: 'gift_wrap', // Server looks up actual price }), }); if (!response.ok) { const body = await response.json(); throw new Error(body.message); } }; try { const response = await actions.runServerUpdate(updateOnServer); if (response.type === 'error') { // Handle Stripe API errors (for example, session retrieval failure) } } 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. } }); } ``` #### React ```jsx import React from 'react'; import {useCheckoutElements} from '@stripe/react-stripe-js'; export const AddServiceButton = () => { const checkoutState = useCheckoutElements(); if (checkoutState.type === 'loading') { return (
Loading...
); } else if (checkoutState.type === 'error') { return (
Error: {checkoutState.error.message}
); } const {runServerUpdate, id} = checkoutState; const addService = async () => { const updateOnServer = async () => { const response = await fetch('/update-custom-amount', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ checkout_session_id: id, product_id: 'gift_wrap', // Server looks up actual price }), }); if (!response.ok) { const body = await response.json(); throw new Error(body.message); } }; try { const res = await runServerUpdate(updateOnServer); if (res.type === 'error') { // Handle Stripe API errors (for example, session retrieval failure) } } 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 ( ); }; ``` ## サーバーエンドポイントの作成 [サーバー側] サーバーで金額を計算し、入力を検証します。その後、`line_items` を [price_data](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-line_items-price_data) で更新して、単発の請求を追加できます。 > `line_items` または `price_data` を更新すると、Session の合計と税金が再計算されます。 #### Node ```node import express from 'express'; import Stripe from 'stripe'; const app = express(); app.use(express.json()); // Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. const stripe = new Stripe('<>'); // Product catalog with prices - store this securely server-side const PRODUCTS = { gift_wrap: { name: 'Gift Wrap', price: 500 }, // $5.00 express_shipping: { name: 'Express Shipping', price: 1500 }, // $15.00 warranty: { name: 'Extended Warranty', price: 2000 }, // $20.00 }; app.post('/update-custom-amount', async (req, res) => { try { const {checkout_session_id, product_id} = req.body; const session = await stripe.checkout.sessions.retrieve(checkout_session_id); if (session.status === 'complete' || session.expires_at * 1000 < Date.now()) { return res.status(400).json({error: 'Session is no longer updatable.'}); } // Look up product price server-side const product = PRODUCTS[product_id]; if (!product) { return res.status(400).json({error: 'Invalid product ID'}); } // Add the additional product via price_data const updated = await stripe.checkout.sessions.update(checkout_session_id, { line_items: [ { price_data: { currency: 'usd', product_data: {name: product.name}, unit_amount: product.price, }, quantity: 1, }, ], }); return res.json({id: updated.id, amount_total: updated.amount_total}); } catch (err) { return res.status(400).json({error: err.message}); } }); app.listen(4242, () => console.log('Server running on port 4242')); ``` # Payment Intents API > This is a Payment Intents API for when payment-ui is elements. View the full page at https://docs.stripe.com/payments/advanced/dynamically-update-amounts?payment-ui=elements. ## サーバーで合計を再計算して更新する [サーバー側] 合計金額を更新するには、以下のようにします。 1. クライアントからカートまたは選択内容への変更を取得します。 1. サーバーで新しい合計金額を再計算します。 1. 新しい金額で PaymentIntent を更新します。 1. クライアントに PaymentIntent (またはその `client_secret`) を返します #### Node ```node import express from 'express'; import Stripe from 'stripe'; const app = express(); app.use(express.json()); // Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. const stripe = new Stripe('<>'); function computeOrderAmount(items, options = {}) { // ToDo: Your logic to compute an order total } app.post('/update-payment-intent', async (req, res) => { try { const {payment_intent_id, items, shipping_cents, service_cents, discount_cents} = req.body; // Compute amount on server const amount = computeOrderAmount(items, {shipping_cents, service_cents, discount_cents}); // Update the amount if the PaymentIntent can be updated const pi = await stripe.paymentIntents.update(payment_intent_id, { amount, }); return res.json({id: pi.id, amount: pi.amount, client_secret: pi.client_secret}); } catch (err) { return res.status(400).json({error: err.message}); } }); app.listen(4242, () => console.log('Server running on port 4242')); ``` ## クライアントを更新して確定する [クライアント側] > クライアントとサーバーの同期を維持する責任があります。 サーバーで PaymentIntent の金額を更新したら、UI を更新し、顧客の準備ができたら確定します。