---
title: "Stripe Checkoutの実装方法：ui_mode別のセッション作成とWebhookでの注文確定"
url: "https://www.issoh.co.jp/tech/details/18533/"
published: 2026-10-18
updated: 2026-10-19
categories: ["開発"]
publisher: "株式会社一創"
---

# Stripe Checkoutの実装方法：ui\_mode別のセッション作成とWebhookでの注文確定

Stripe Checkoutは、Stripeが用意した決済画面にサーバーで作ったCheckout Sessionを渡して表示する仕組みです。カード入力欄・3Dセキュア・決済手段の切り替えをStripe側が持つため、自社で書くのはセッションの作成と、決済後の注文確定だけになります。この記事では、2026-03-25.dahlia版で改名された4つのui\_modeの違い、Node.jsでのホスト型と埋め込み型の実装コード、Webhookによる注文確定、ElementsやPayment Linksとの選び分けまでを解説します。仕様の確認元は2026年10月時点の公式ドキュメントです。

## まとめ｜Stripe Checkoutで決めるui\_modeとWebhookでの注文確定の要点

最初に決めるのは表示場所です。Stripeのページへ遷移させるなら`hosted_page`、自社ページに決済画面ごと埋め込むなら`embedded_page`を選びます。2026-03-25.dahlia以降のAPIバージョンでは旧値の`hosted`や`embedded`を送るとエラーになるため、古い記事のコードはそのまま動きません。

金額と商品はサーバー側の注文データから`line_items`に入れ、画面からは受け取りません。注文の確定は戻り先ページではなく、`checkout.session.completed`のWebhookで行います。

単発販売やサブスクリプションの開始ならCheckoutで足ります。購入フローの途中に決済欄を溶け込ませたい場合や、与信だけ先に取る予約型の決済が必要な場合に限り、Elementsや[PaymentIntentsの直接実装](https://www.issoh.co.jp/tech/details/18527/)へ進みます。

## Stripe Checkoutの決済画面とセッションで指定する商品・金額の仕組み

Checkoutの実装は、Checkout Sessionというオブジェクトを1回の購入ごとに作ることから始まります。

### Checkout Sessionが決める商品・金額・有効期限30分〜24時間の範囲

[Checkout Session作成のAPIリファレンス](https://docs.stripe.com/api/checkout/sessions/create)によると、セッションには商品（`line_items`）、取引の種類（`mode`）、表示方法（`ui_mode`）、決済後の戻り先を指定します。`mode`は1回限りの決済の`payment`、継続課金を始める`subscription`、カードを保存だけする`setup`の3種類です。

セッションには期限があります。`expires_at`は作成から30分〜24時間の範囲で指定でき、省略すると24時間です。在庫を確保してから決済させる販売では、期限を短くして売れ残りの引当を早く解きます。`client_reference_id`（最大200文字）に自社の注文IDを入れておくと、後の照合が簡単です。

### 日本のアカウントでCheckoutを使う費用：カード3.6%と追加料金

[Stripeの日本向け料金ページ](https://stripe.com/jp/pricing)では、2026年10月時点でカードとウォレットの決済手数料を3.6%としています。Checkoutで決済を受け付けること自体に追加料金はかかりません。

有料になるのはオプションです。単発購入のあとに請求書を自動発行する機能は取引額の0.4%（1件あたり上限2.00米ドル）、決済ページを独自ドメインで出す機能は月額10米ドルと記載されています。通貨換算が発生する決済では2%が加算されます。海外向けに円以外で売る場合は、この加算分も粗利の計算に含めてください。手数料体系の全体像は親記事の[Stripeとは？仕組み・手数料・実装パターン](https://www.issoh.co.jp/tech/details/15416/)で整理しています。

## 2026-03-25.dahlia以降のui\_mode4種類とホスト型・埋め込み型の違い

Checkoutの実装方式は`ui_mode`の値で決まります。この値の名前が2026年3月に変わっているため、まず現行の4値を押さえます。

### ui\_modeの4種類を決済画面の表示場所と実装工数で比較した選定基準

[Checkoutの公式ガイド](https://docs.stripe.com/payments/checkout)は、Checkout Sessions APIで使える決済UIをフルページ（推奨）・埋め込みフォーム・Elementsに分けています。APIリファレンスの`ui_mode`と対応させると次のとおりです。

| ui\_mode         | 表示場所          | 見た目の調整         | 工数  |
| ---------------- | ------------- | -------------- | --- |
| hosted\_page（既定） | Stripeのページへ遷移 | ブランド設定15項目     | 小   |
| embedded\_page   | 自社ページ内にページごと  | ブランド設定15項目     | 小〜中 |
| form             | 自社ページ内のフォーム   | Appearance API | 中   |
| elements         | 自社UIに部品を配置    | CSSまで自由        | 大   |

実務では`hosted_page`から検討を始めれば十分です。注文概要・クーポン入力・無料トライアルの表示まで標準で付き、保守の手間も公式ガイドの比較表で「低」とされています。`elements`はセッションの管理をStripeに任せつつ画面を自作する方式で、注文概要は自社で作る必要があります。

### 旧値hosted・embeddedと旧メソッドで失敗する移行時の注意点

[2026-03-25.dahlia版の変更履歴](https://docs.stripe.com/changelog/dahlia/2026-03-25/updates-available-checkout-session-ui-modes)では、`hosted`・`embedded`・`custom`を削除し、`hosted_page`・`embedded_page`・`elements`を追加したとしています。`form`もこの版で加わりました。新しいAPIバージョンで旧値を送ると、セッションの作成は失敗します。

ブラウザ側のメソッドも同じ版で名前が変わりました。[埋め込み型の初期化メソッドの変更履歴](https://docs.stripe.com/changelog/dahlia/2026-03-25/rename-init-embedded-checkout-to-create-embedded-checkout-page)によると、`initEmbeddedCheckout()`は`createEmbeddedCheckoutPage()`になり、旧名を呼ぶと`IntegrationError`が出ます。Elements方式の`initCheckout()`も`initCheckoutElementsSdk()`へ改名されています。

さらに古いコードでよく見る`stripe.redirectToCheckout()`は、[2025-09-30.clover版で削除](https://docs.stripe.com/changelog/clover/2025-09-30/remove-redirect-to-checkout)済みです。代わりにセッションの`url`へ通常のリダイレクトで移動させます。2025年以前の解説記事を写す場合は、この3点を必ず置き換えてください。

## ホスト型Checkoutを試す手順：Node.jsでセッションを作り303で遷移

最も少ないコードで動くのが`hosted_page`です。[ホスト型Checkoutのクイックスタート](https://docs.stripe.com/checkout/quickstart)と同じ構成で、自社の注文データから金額を渡す形に直した例を示します。

### stripe-node 23系とExpressによる決済セッション作成コード

[Stripe公式のNode.jsライブラリ](https://github.com/stripe/stripe-node)を`npm install stripe express`で入れ、次のエンドポイントを置きます。購入ボタンはこのURLへPOSTするだけのformで構いません。`findOrder`は自社の注文取得処理を表す仮の関数です。

```
import express from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2026-09-30.endive', // ui_mode の新しい値が使える版を明示する
});
const app = express();
const DOMAIN = 'https://example.com';

app.post('/create-checkout-session', express.urlencoded({ extended: false }), async (req, res) => {
  // 金額は画面から受け取らず、サーバー側の注文データから決める
  const order = await findOrder(req.body.orderId);

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    // ui_mode を省略すると hosted_page（Stripeの決済ページへ遷移）
    line_items: order.items.map((item) => ({
      price_data: {
        currency: 'jpy',
        unit_amount: item.unitPriceYen, // 日本円は小数点なし。1,980円なら1980
        product_data: { name: item.name },
      },
      quantity: item.quantity,
    })),
    client_reference_id: order.id,
    expires_at: Math.floor(Date.now() / 1000) + 60 * 60, // 1時間で失効させる
    success_url: `${DOMAIN}/orders/complete?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${DOMAIN}/cart`,
  });

  res.redirect(303, session.url);
});
```

303で返すのは、formのPOSTに対してブラウザにGETでStripeのページを開かせるためです。`cancel_url`を指定すると決済ページに戻るボタンが表示され、押した利用者はカート画面に戻ります。

### line\_itemsの上限100件とprice\_dataでサーバー側から金額を渡す方法

商品の渡し方は2通りあります。ダッシュボードやAPIで登録済みのPrice IDを`price`に入れる方法と、上の例のように`price_data`でその場の金額を渡す方法です。商品点数が少なく価格が固定ならPrice IDが管理しやすく、在庫システムの価格を正にしたいなら`price_data`が向いています。

APIリファレンスでは、`payment`モードの`line_items`は最大100件で、数十件を超えるならまとめるよう推奨しています。`subscription`モードは定期課金の品目20件・単発の品目20件が上限です。B2Bの一括発注で明細が100行を超える場合は、明細を「ご注文一式」の1行に集約し、内訳は自社の注文画面に残す設計にします。

### success\_urlのCHECKOUT\_SESSION\_IDで戻り先ページに結果を出す

`success_url`に`{CHECKOUT_SESSION_ID}`という文字列を含めると、Stripeが実際のセッションIDに置き換えてから利用者を戻します。戻り先ページのサーバー処理では、このIDで`stripe.checkout.sessions.retrieve()`を呼び、`status`が`complete`かを確かめてから「ご注文ありがとうございます」を表示します。

注意したいのは、このページを注文確定の唯一の入口にしないことです。決済直後に通信が切れると、利用者は戻り先ページに到達しません。確定処理は後述のWebhookでも必ず呼ぶ構成にします。

## 埋め込み型Checkoutの実装：セッション作成とフォーム描画の手順

自社サイトのドメインから離れずに決済させたい場合は`embedded_page`を使います。サーバーはURLの代わりに`client_secret`を返し、ブラウザ側でフォームを組み立てます。

### embedded\_pageの指定とクライアント用シークレットを返すAPI

[埋め込み型Checkoutのクイックスタート](https://docs.stripe.com/checkout/embedded/quickstart)に沿ったサーバー側のコードです。前の例の`stripe`・`app`・`DOMAIN`をそのまま使います。

```
app.post('/api/checkout-sessions', express.json(), async (req, res) => {
  const order = await findOrder(req.body.orderId);

  const session = await stripe.checkout.sessions.create({
    ui_mode: 'embedded_page',
    mode: 'payment',
    line_items: [{ price: order.priceId, quantity: order.quantity }],
    client_reference_id: order.id,
    // embedded_page では success_url と cancel_url は指定できない
    return_url: `${DOMAIN}/orders/return?session_id={CHECKOUT_SESSION_ID}`,
  });

  res.json({ clientSecret: session.client_secret });
});
```

APIリファレンスでは、`success_url`と`cancel_url`は`embedded_page`と`elements`で指定できないパラメータです。代わりに`return_url`を使います。ホスト型のコードを流用して両方を残すと、作成時にエラーになります。

### endive版Stripe.jsを使ったブラウザ側の決済フォーム描画

ブラウザ側は、Stripe.jsを`js.stripe.com`から読み込み、`createEmbeddedCheckoutPage()`に`client_secret`を取得する関数を渡します。クイックスタートでは、Stripe.jsをバンドルに含めたり自社でホストしたりしないよう求めています。

```
<script src="https://js.stripe.com/endive/stripe.js"></script>
<div id="checkout"></div>
<script type="module">
  const stripe = Stripe('pk_test_xxxxxxxx'); // 公開可能キーだけを置く

  const fetchClientSecret = async () => {
    const res = await fetch('/api/checkout-sessions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ orderId: 'A-10293' }),
    });
    const { clientSecret } = await res.json();
    return clientSecret;
  };

  // 2026-03-25.dahlia で initEmbeddedCheckout から改名されたメソッド
  const checkout = await stripe.createEmbeddedCheckoutPage({ fetchClientSecret });
  checkout.mount('#checkout');
</script>
```

フォームはiframeで描画されます。ガイドは、このフォームをさらに別のiframeの中に置かないよう注意しています。PayPayのようにアプリへ遷移して戻る決済手段が動かなくなるためです。

### return\_urlのstatusでopenとcompleteを分岐し再表示する処理

決済を終えた利用者は`return_url`へ戻ります。戻り先ページではセッションを取得し、`status`が`complete`なら完了画面、`open`なら決済が失敗または中断されたものとして、もう一度フォームをマウントします。

カード決済だけなら画面遷移なしで終えたいという要件には、`redirect_on_completion`を使います。既定の`always`は常に戻り先へ移動し、`if_required`はリダイレクトが必要な決済手段のときだけ移動します。`never`にするとリダイレクト型の決済手段は使えなくなる点に注意してください。

## Webhookによる注文確定：完了イベントと非同期決済の入金確認

ホスト型でも埋め込み型でも、注文の確定方法は同じです。[注文のフルフィルメントの公式ガイド](https://docs.stripe.com/checkout/fulfillment)は、戻り先ページだけに頼る実装を明確に否定しています。

### 決済完了と非同期の入金成功イベントを受ける冪等なfulfill関数

同ガイドによると、受けるイベントは`checkout.session.completed`と、コンビニ決済や銀行振込のように入金が後から確定する決済手段向けの`checkout.session.async_payment_succeeded`です。確定処理の関数は、同じセッションIDで複数回、ときには同時に呼ばれる前提で作るよう求めています。

```
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    // 署名検証には JSON 化する前の生のボディを渡す
    event = stripe.webhooks.constructEvent(
      req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch {
    return res.status(400).send('invalid signature');
  }

  if (event.type === 'checkout.session.completed' ||
      event.type === 'checkout.session.async_payment_succeeded') {
    await fulfillCheckout(event.data.object.id);
  }
  res.sendStatus(200);
});

async function fulfillCheckout(sessionId) {
  const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['line_items'] });
  if (session.payment_status === 'unpaid') return; // 非同期決済の入金待ち

  // 注文IDに一意制約を置き、2回目以降の呼び出しでは何もしない
  const firstTime = await markOrderPaidOnce(session.client_reference_id, session.id);
  if (!firstTime) return;
  await startShipping(session.client_reference_id, session.line_items.data);
}
```

`markOrderPaidOnce`と`startShipping`は自社の処理を表す仮の関数です。コンビニ決済で`completed`が届いた時点では`payment_status`がまだ`unpaid`なので、ここで発送を始めないのが分岐の目的です。入金待ちの扱いは[Stripeの決済方法ごとの有効化とWebhook処理](https://www.issoh.co.jp/tech/details/18531/)、署名検証の考え方は[決済Webhookの署名検証と冪等な注文確定](https://www.issoh.co.jp/tech/details/15734/)で詳しく扱っています。

### Webhook応答を最大10秒待つ遷移とstripe listenでのローカル検証

ガイドに記載された仕様のうち、画面遷移を設計する際に見落としやすいのが次の1点です。`checkout.session.completed`を受けるWebhookエンドポイントがあり、`success_url`も設定している場合、Checkoutはサーバーの応答を最大10秒待ってから利用者を戻します。上のコードのように確定処理を終えてから200を返す作りだと、処理が重いほど利用者の待ち時間が延びます。在庫連携や帳票作成のように時間がかかる処理は、キューに積んで先に200を返すほうが安全です。

ローカルでは、Stripe CLIの`stripe listen --forward-to localhost:4242/webhook`でイベントを手元のサーバーへ転送できます。表示される`whsec_`で始まる値を署名検証の鍵に設定し、[Stripeのテストカード](https://www.issoh.co.jp/tech/details/18523/)で決済を通すと、成功・3Dセキュア・拒否の各経路を確認できます。

## CheckoutとElements・Payment Linksの選び分けと採用を見送る場面

Checkoutは「決済画面を作らない」ための仕組みです。画面を作り込みたい要件が増えるほど、他の方式との境目を意識する必要があります。

### Checkoutで止めるべき条件：単発販売・サブスク開始・標準の配送と税

次のいずれかに当てはまるなら、Checkoutの`hosted_page`か`embedded_page`で止めます。商品をカートに入れて1回で払う単発販売、月額プランの申し込み、配送先住所の入力と自動の税計算が標準機能の範囲で済む販売です。どれも作成時のパラメータで対応でき、画面のコードは書きません。

カード情報が自社サーバーを通らないため、PCI DSSの自己問診も軽い区分に収まります。準拠範囲の考え方は[PCI DSSの要件と準拠レベル](https://www.issoh.co.jp/column/details/13185/)を参照してください。サブスクリプションの更新失敗や解約の扱いまで設計する場合は、[サブスクリプション課金の実装](https://www.issoh.co.jp/tech/details/15728/)の論点が加わります。

コードを1行も書かずに売りたいだけなら、Checkoutより先にPayment Linksを検討します。フルフィルメントのガイドにもあるとおり、Payment LinksはCheckoutの上に作られた機能で、決済後の処理は同じWebhookで書けます。Payment Linksの作成手順とリンク経由の注文の判別は[Stripe Payment Linksの使い方](https://www.issoh.co.jp/tech/details/18537/)で解説しています。

### Elementsへ進む条件と、Checkoutを採用しない予約・与信の場面

Checkoutを見送るのは、購入フローの1ステップとして決済欄を自社デザインに溶け込ませたい場合です。ブランド設定15項目では足りず、入力欄の配置まで変えたいなら`elements`か、PaymentIntentsとPayment Elementの直接実装に進みます。

与信だけ先に取り、宿泊やサービスの提供後に金額を確定するような予約型の決済も、Checkoutの標準の流れには合いません。金額が注文処理の途中で変わる多段の購入フローも同様です。この場合は[Stripe APIでPaymentIntentを作る実装](https://www.issoh.co.jp/tech/details/18527/)のように、ステータス遷移を自社で扱う設計になります。逆に、見た目を少し変えたいだけでElementsへ移るのは過剰です。保守するコードが一気に増えます。

### 決済と基幹システムの連携まで含めて外部に設計を任せる条件と判断の目安

Checkoutのコード自体は短く収まります。難しいのは、決済の結果を在庫・会計・基幹システムへ反映する部分です。Webhookの重複や順序違い、コンビニ決済の入金待ち、返金時の在庫戻しを取りこぼすと、二重発送や入金済み未反映の問い合わせ対応に追われます。

社内に決済実装の経験者がいない状態で、既存の販売管理システムとの連携まで同時に進める場合は、設計段階から外部の知見を入れると手戻りが減ります。一創では[決済・サブスクリプションシステムの開発](https://www.issoh.co.jp/service/system/payment/)として、Stripe Checkoutの導入から基幹システムとの連携までを支援しています。

## Stripe Checkoutのよくある質問｜表示方式・リンク販売・有効期限

Stripe Checkoutを実装するときによく出る質問をまとめました。

### Stripe CheckoutとPayment Linksの違いは何ですか？

Payment LinksはCheckoutの決済画面を、コードを書かずに共有用のURLとして発行する機能です。ダッシュボードで商品と価格を選ぶだけでリンクができ、SNSやメールに貼って販売できます。一方Checkoutは、購入ごとにサーバーでセッションを作るため、カートの中身や注文IDを決済に結び付けられる仕組みです。自社サイトの注文と決済を照合したいならCheckoutを選びます。

### Stripe Checkoutの決済画面を日本語で表示できますか？

表示できます。セッション作成時の`locale`を省略するか`auto`にすると、利用者のブラウザの言語設定に合わせて表示されます。利用者のブラウザの言語設定にかかわらず、常に日本語で出したい場合に指定する値は`ja`です。送信ボタンの文言は`submit_type`に`pay`・`book`・`donate`・`subscribe`のいずれかを指定して切り替えられます。

### Checkout Sessionの有効期限は変更できますか？

変更できます。作成時の`expires_at`にUNIX時間（秒）で、作成から30分後〜24時間後の範囲の時刻を指定します。省略した場合の期限は24時間です。期限が切れたセッションでは決済できないため、利用者がやり直すときは新しいセッションを作ります。在庫を引き当てる販売では、期限を短めにすると引当の解除が早まります。

### Stripe Checkoutでサブスクリプションを始められますか？

サブスクリプションを開始する用途にも対応しており、セッション作成時に課金のモードと品目を指定します。`mode`を`subscription`にし、`line_items`に継続課金のPriceを入れます。このモードではCheckoutが顧客（Customer）を自動で作るため、次回以降の請求も同じ顧客に対して行う仕組みです。定期課金の品目に設定できる件数の上限は20件です。更新後の請求失敗や解約の処理は、Checkoutの外で別途実装します。Priceの設計やWebhookによる利用権の同期、カスタマーポータルの組み込みは[Stripe Billingの実装手順](https://www.issoh.co.jp/tech/details/18535/)で解説しています。

### 古いredirectToCheckoutのコードはどう直せばよいですか？

`stripe.redirectToCheckout()`は2025-09-30.clover版でStripe.jsから削除されています。サーバーでCheckout Sessionを作り、返ってきた`url`へ303で遷移させるか、ブラウザ側で`window.location.href`に代入して移動させます。同時に`ui_mode`の旧値や`initEmbeddedCheckout()`が残っていないかも確認してください。

## 関連記事

- [Stripeとは？決済プラットフォームの仕組み・手数料・実装パターンを開発視点で解説](https://www.issoh.co.jp/tech/details/15416/)：Payment Links・Checkout・Elementsの位置付けと手数料の全体像。
- [Stripe APIの使い方：PaymentIntentの作成・APIキー認証・冪等キーをNode.jsとcurlで実装](https://www.issoh.co.jp/tech/details/18527/)：Checkoutを使わずに決済を直接作る場合の実装。
- [決済Webhookとは？仕組み・主要イベントと署名検証・冪等な注文確定の実装を開発視点で解説](https://www.issoh.co.jp/tech/details/15734/)：checkout.session.completedを受ける側の設計。
- [Stripe PayPay決済の実装手順：有効化・Checkout・PaymentIntentのコードと使えない機能](https://www.issoh.co.jp/tech/details/18529/)：CheckoutでPayPayを受け付ける場合の制約。
- [Stripeテストカード一覧と使い方：3Dセキュア・決済拒否・不審請求をテストする手順](https://www.issoh.co.jp/tech/details/18523/)：Checkoutの各経路を試すカード番号。

---

出典: [Stripe Checkoutの実装方法：ui\_mode別のセッション作成とWebhookでの注文確定](<https://www.issoh.co.jp/tech/details/18533/>)（株式会社一創）
