1. なぜ ACP が必要で、「ただの REST API」ではないのか
皮肉な見方をすると、ACP は平凡な HTTP エンドポイントと JSON 構造の寄せ集めに見えます。/checkout_sessions があり、webhook があり、トークンがある。「OK、またどこかのプラットフォームが作ったカスタム API だね」と思うのは簡単です。しかし ACP の発想はもっと深いところにあります。
ACP は、AI プラットフォーム(例: ChatGPT)、あなたのコマース・バックエンド、そして決済プロバイダという3者の間のオープンなプロトコルとして設計されています。目的は、商品や価格の表現方法、ユーザーの購入意思を AI がどう宣言するか、checkout セッションがどう作成されるか、支払いがどう実行されるか、そして全参加者が最終ステータスをどう知るかを標準化することです。
要点はこうです。ACP を実装した同じマーチャントのバックエンドは、ChatGPT だけでなく、この標準をサポートする他の LLM プラットフォームでも動作する可能性があります。つまり「ChatGPT 専用 API」を書くのではなく、次世代のコマース統合プロトコルを実装するのです。
ChatGPT の Instant Checkout は、ACP 標準の最初の大規模実装です。 ChatGPT はこのプロトコルに従ってあなたの ACP エンドポイントを呼び出し、ユーザーに優れた UI を提示しますが、ルール自体は ACP の仕様で定義されており、「GPT の魔法」がブラックボックスの内側に隠れているわけではありません。
2. ACP の三本柱: Product Feed、Agentic Checkout、Delegated Payment
ACP には、以後たびたび言及する三つの主要仕様があります。
| 仕様 | 役割 | GiftGenius での位置付け |
|---|---|---|
| Product Feed Spec | 商品フィードの形式とフィールド(SKU、価格、在庫、リンク、フラグ)。 | OpenAI がインデックスするギフトの JSON/CSV フィード。 |
| Agentic Checkout | checkout_session のための REST コントラクト(作成、更新、完了)。 | 私たちの ACP バックエンド: /checkout_sessions エンドポイントと webhook。 |
| Delegated Payment | 決済データを委譲トークンとしてマーチャントへ渡す方法。 | 支払い完了時の Stripe Shared Payment Token の取り扱い。 |
Product Feed は前回の講義で扱いました。ここからは残りの二つ、Agentic Checkout と Delegated Payment に注目します。
三つのレイヤーを区別することが重要です。
- 標準(SPEC)。公式文書は、必要なフィールドやエンドポイント、正当なステータス、提供すべき保証を定義します。
- アーキテクチャパターン(ARCH)。たとえば、SKU と注文を別テーブルにする、ACP のラッパーサービスを用意する、webhook 用キューを使うなど。良いプラクティスですが標準の一部ではありません。
- 具体実装(GiftGenius の例)。学習用プロジェクトとしての私たちのテーブル設計、TypeScript の型名、注文のロギング方法など。これはあくまで「例」であり、規範文書ではありません。
どこまでが SPEC で、どこからがあなたのアーキテクチャなのかを常に強調します。そうしないと「講義で persona_tags というフィールドを見たから公式仕様の一部だと思った」といった誤解が起きかねません。
3. checkout_session の内部: 構造とステータス
Agentic Checkout Spec の中心オブジェクトが、あなたのバックエンドにある checkout_session です。論理的には購入の状態であり、どの商品をどの金額で、どの配送オプションで購入しようとしているか、支払いの試行が今どのステータスにあるかを表します。
仕様は checkout_session の必須フィールドを概ね次のように定義しています(原文を簡略化しています)。
- id — あなたが生成して返すセッションの文字列 ID。ChatGPT は以後の呼び出しでこれを使用します。
- buyer — 購入者情報(氏名、メール、電話、場合により住所)。PSP やあなたのシステムが信頼して使えるよう、実際の仕様では構造化されています。
- status — 現在の購入状態を表す文字列の列挙型。基本ステータス:
- not_ready_for_payment — まだ支払い不可(例: 配送オプション未選択、税の再計算未完了)。
- ready_for_payment — 準備完了。決済トークンをリクエストして請求可能。
- completed — 支払い成功、注文作成済み。
- canceled — 購入がキャンセル(ユーザー操作またはエラー)。
- currency — ISO 4217 の小文字通貨コード("usd"、"eur" など)。
- line_items — カート内の明細リスト。各項目に SKU、数量、算出済み金額を持つ。
- fulfillment_address — 配送先住所(該当する場合)。
- fulfillment_options と fulfillment_option_id — 利用可能な配送(または実行)オプションと現在選択中のオプション。
- totals — 集計金額(商品小計、税、配送、合計)。
- order — セッション完了後に作成される注文を表すオブジェクト。
- messages — ChatGPT が購入者に表示できるユーザーメッセージ(注意やエラーなど)。
- links — 返品ポリシー、Privacy Policy、Terms of Service などへのリンク一覧。
デモでは全フィールドの実装は不要ですが、重要なのは考え方です。checkout_session は「ひとつの購入試行の履歴と現在の状態」であり、ChatGPT は適切な UX に必要な情報がそこに揃っていることを期待します。
分かりやすくするため、学習用コードでは簡略化した型を導入します。
// GiftGenius 用の簡易 checkout_session モデル(正式 SPEC の完全版ではない)
type GGCheckoutStatus = 'not_ready_for_payment' | 'ready_for_payment' | 'completed' | 'canceled';
type GGLineItem = { skuId: string; quantity: number; total: number };
type GGCheckoutSession = {
id: string;
status: GGCheckoutStatus;
currency: 'usd';
lineItems: GGLineItem[];
grandTotal: number;
};
このモデルは公式より単純ですが、実践には十分です。多数のフィールドに溺れず、ステータスと遷移を頭に入れて学べます。
4. checkout_session のライフサイクル
Agentic Checkout の仕様は checkout_session に対するいくつかの操作を定義しています。簡略化するとライフサイクルは次のとおりです。
- セッション作成: POST /checkout_sessions。
- セッション更新: POST /checkout_sessions/{id}。
- セッション完了(complete): POST /checkout_sessions/{id}/complete。
- (場合により)キャンセル: 専用の cancel エンドポイント、または更新で canceled へ遷移。
ステートの観点では、次のような図にできます。
stateDiagram-v2
[*] --> not_ready_for_payment
not_ready_for_payment --> ready_for_payment: 配送/税の計算
オプションの選択
ready_for_payment --> completed: 成功した POST /complete
ready_for_payment --> canceled: ユーザーによる取消 または エラー
not_ready_for_payment --> canceled: エラー、不整合なデータ
作成直後の checkout_session は通常 not_ready_for_payment から始まります。すでに支払いに必要な情報がそろっていれば(例: 配送も税も不要なデジタル商品)、直ちに ready_for_payment にできます。 更新は、住所やプロモコード、配送オプションなどの追加と再計算に使います。 完了は Delegated Payment が動き、実際に請求が行われる瞬間です。
ここで役割分担を理解することが重要です。
- ChatGPT は、ユーザーとの対話に基づいてセッションの作成・更新・完了を開始します。
- あなたのバックエンド(マーチャント)は、SKU の検証、在庫、価格と税の計算、ステータス遷移、注文作成といったビジネスロジックを担います。
- PSP(Stripe など)は実際の決済を処理し、マーチャントが請求に使う Shared Payment Token を発行します。
このステート図に具体的な HTTP リクエストと小さなコード例を重ねていきます。
5. checkout_session の作成: ChatGPT が何を期待しているか
ChatGPT(またはエージェント)がユーザーの実購入意思を認識すると、Product Feed に基づいて line items(SKU、数量、想定通貨、配送に関する希望など)を組み立て、あなたの POST /checkout_sessions エンドポイントを呼びます。
マーチャント側で必要な処理は次のとおりです。
- 入力検証: すべての SKU が存在し販売可能か、ポリシーに反していないか(例: 未成年への酒類)を確認します。
- 自社ルールに基づいて価格と税を計算します。
- 物理商品であれば配送オプション(fulfillment options)を用意します。
- ステータスと金額を含む正しい checkout_session を返します。
GiftGenius のための最小限の Express ハンドラは次のようになります。
// 簡易な checkout_session 作成の擬似コード
app.post('/checkout_sessions', async (req, res) => {
const items = req.body.lineItems as GGLineItem[]; // skuId + quantity
const pricedItems = await priceItems(items); // 各 SKU の total を算出
const grandTotal = sum(pricedItems.map(i => i.total));
const session: GGCheckoutSession = {
id: generateId(),
status: 'ready_for_payment', // デジタルギフトなら直ちに支払い可能にできる
currency: 'usd',
lineItems: pricedItems,
grandTotal,
};
res.status(201).json(session);
});
ここでは次の点を行っています。
- クライアント(ChatGPT)からの入力価格を信用せず、自社データで再計算します。これはコマースの安全性に極めて重要です。
- 自前のセッション id を生成します(例: gg_chk_... のような接頭辞)。
- 追加の手順がなければ(配送なし、自動税計算、単純なモデルなど)、ステータスを ready_for_payment で返します。
実運用の ACP 準拠バックエンドでは、さらに messages や links、複合の totals を返し、order も(下書きでも)埋めます。これらは仕様で定義されています。
6. checkout_session の更新と冪等性
セッション作成後、ChatGPT はユーザーに追加情報(配送先、クーポン適用、実行オプション変更など)を尋ねることがあります。情報がそろうと、プラットフォームはあなたの POST /checkout_sessions/{id} を呼び、再計算を求めます。
コード的には作成時とほぼ同じですが、新規セッション生成ではなく次を行います。
- id で既存セッションを見つける。
- 変更の適用(例: fulfillment_option_id の変更や割引の追加)。
- 金額の再計算。
- 更新済みの checkout_session を返す。
仕様はリトライ(ネットワーク障害や ChatGPT 側の再送)を許容します。 そのため、以前のモジュールで扱ったツールや webhook の冪等性と同様に、リクエストヘッダーの Idempotency-Key を利用し、重複呼び出しを正しく処理することが推奨されます。
更新ハンドラの一例は次のようになります。
app.post('/checkout_sessions/:id', async (req, res) => {
const id = req.params.id;
const key = req.header('Idempotency-Key'); // 同一 key => 同一効果
const existing = await loadSessionWithIdempotency(id, key, req.body);
// applyUpdates 内で価格や配送などを再計算する
const updated = await applyUpdates(existing, req.body);
await saveSession(updated, key);
res.json(updated);
});
ここでは SPEC の厳密な形にこだわらず、考え方を示しています。入力は変更点と冪等キー、出力は一貫した checkout_session の状態です。 同じキーの同一リクエストが来たら、同じ結果を返し、余計な注文やログの重複を生まないようにします。
7. checkout_session の完了と Delegated Payment: Shared Payment Token の仕組み
最も重要で緊張する瞬間は checkout_session の完了、すなわち実際に請求が行われるときです。ここで二つ目の仕様 Delegated Payment が登場します。
Delegated Payment のアイデア
ユーザーは ChatGPT の UI で決済情報(カード、ウォレット、保存済みの支払い方法)を入力または選択します。プラットフォームはこれらの情報をあなたに直接送るのではなく、PSP(例: Stripe)に Shared Payment Token(SPT)を要求して取得します。SPT は次の性質を持ちます。
- マーチャントと特定のセッションに一意に結びつく。
- 金額と有効期限で制限される。
- 実際のカード番号をあなたに開示しない。
結果として、次のような分担になります。
| アクター | カードの決済情報を閲覧 | Shared Payment Token を閲覧 | 注文詳細(SKU・金額)を閲覧 |
|---|---|---|---|
| ユーザー | はい(UI で入力) | いいえ(不要) | 一部(何をいくらで買うか) |
| ChatGPT/OpenAI | はい(支払い処理の過程で) | はい | はい |
| PSP(Stripe) | はい | はい | 支払いの範囲内 |
| マーチャント | いいえ | はい | はい |
この設計により、マーチャントは決済情報を保持せずに、注文のビジネスロジックへ集中できます。コンプライアンスは PSP とプラットフォームに委ねられます。
Insight
Shared Payment Token のポイントは、あなたのバックエンドからカード情報を隠しつつ、請求自体はあなたが行えることです。別の見方もできます。
ホテルやショップで、まずカードにホールド(与信枠の確保)をかけ、その後に請求されるケースに心当たりがあるはずです。Shared Payment Token をホールド用のトークンだと考えてください。ChatGPT はユーザーの口座にホールドをかけ、まだ請求はしていません。そのホールドトークンをあなたに渡し、あなたはそれを Stripe に送って請求できます。
ここで二つの重要な注意点があります。
- ホールド額と請求額は大きく乖離すべきではなく、可能なら一致させるべきです。
- ChatGPT 経由で初月 $1 のサブスクリプションを販売し、その後は毎月 $49.99 を請求する、といったモデルも可能です。
リクエスト POST /checkout_sessions/{id}/complete
Instant Checkout の UI でユーザーが支払い確定ボタンを押すと、ChatGPT は次を行います。
- PSP(例: Stripe の ACP API)から SPT を取得。
- そのトークンを購入者データとともに POST /checkout_sessions/{id}/complete であなたのバックエンドに送信。
仕様はリクエストボディを概ね次のように記述しています(以下は公式ドキュメントを簡略化した例)。
POST /checkout_sessions/checkout_session_123/complete
{
"buyer": {
"first_name": "John",
"last_name": "Smith",
"email": "johnsmith@mail.com"
},
"payment_data": {
"token": "spt_123",
"provider": "stripe"
}
}
あなたのバックエンドは次の対応をします。
- checkout_session_123 という id の checkout_session を検索。
- 完了可能なステータスであることを確認(通常は ready_for_payment)。
- PSP で spt_123 を用いて決済を作成(PSP によって API は異なり、Stripe では特定のエンドポイントと支払いメソッドタイプ)。
- 決済の確定を待機。
- checkout_session を completed に更新し、注文を作成・保存、セッションの order を埋める。
- 最新の checkout_session をレスポンスとして返す。
非常に簡略化した TypeScript 擬似コードは次のようになります。
app.post('/checkout_sessions/:id/complete', async (req, res) => {
const { id } = req.params;
const { buyer, payment_data } = req.body;
const session = await loadSession(id);
await chargeWithSharedToken(payment_data.token, session.grandTotal);
const completed = await markSessionCompleted(session, buyer);
res.json(completed);
});
実運用では、この間にエラー処理、再試行、ログ、あなたの注文モデルとの統合などが入ります。
もし問題が起これば(例: 決済が拒否された)、checkout_session を not_ready_for_payment または canceled のステータスで返し、messages を埋めて ChatGPT がユーザーに状況を適切に説明できるようにしてください。
8. ChatGPT の Instant Checkout: すべてが一つのフローに収束する
ここまでを「意図から支払いまで」のシナリオにまとめます。チャット内の「購入」ボタンの裏側で何が起きているかを読み解くつもりで見てください。
簡略シナリオ:
- ユーザー: 「友だち向けのデジタルギフトを $50 以下で選んで、ここでそのまま購入して」。
- エージェント(または ChatGPT App 自身)が Product Feed を使い、予算内の SKU を探す。
- ChatGPT はチャット内で複数のギフトカード(あなたの GiftGenius ウィジェット経由)を表示し、ひとつ選ぶよう促す。
- 選択後、ChatGPT は line items を組み立て、あなたの ACP バックエンドの POST /checkout_sessions を呼び、金額とステータスを持つ checkout_session を取得。
- Instant Checkout の UI に、最終金額、商品名、返品ポリシー、確認ボタンが表示される。
- 確認時、ChatGPT は PSP から Shared Payment Token を取得し、前述のように POST /checkout_sessions/{id}/complete を呼ぶ。
- あなたのバックエンドは決済を実行し、注文を作成し、ステータス completed の checkout_session を返す。
- ChatGPT はユーザーに購入完了を表示し、あなたのバックエンドは(Agentic Checkout Spec に沿った webhook で)OpenAI にイベントを送り、プラットフォームが注文の行方を把握できるようにする。
シーケンス図にすると次のようになります。
sequenceDiagram
actor U as ユーザー
participant GPT as ChatGPT
participant GG as GiftGenius ACP バックエンド
participant PSP as Stripe (PSP)
U->>GPT: $50 以下のギフトがほしい。ここでそのまま購入したい
GPT->>GG: POST /checkout_sessions (line_items)
GG-->>GPT: checkout_session (ready_for_payment)
GPT->>U: Instant Checkout を表示(商品、価格、ToS)
U->>GPT: 「支払いを確定」を押す
GPT->>PSP: マーチャントと金額に対する SPT を要求
PSP-->>GPT: Shared Payment Token (spt_xxx)
GPT->>GG: POST /checkout_sessions/{id}/complete (token + buyer)
GG->>PSP: SPT で決済
PSP-->>GG: 決済成功
GG-->>GPT: checkout_session (completed + order)
GPT-->>U: 購入完了を表示
このシナリオには、あなたの独自 DB を勝手に叩くような「謎の内部エンドポイント」は登場しません。すべては明確に定義された ACP のコントラクトに収まり、各参加者は自分の役割を理解しています。
9. ミニ実践: GiftGenius の簡易 ACP バックエンド
理論で終わらせないために、学習用プロジェクトに ACP レイヤーを実装するイメージを具体化しましょう。
GiftGenius にはすでに次があるとします。
- Product Feed を生成する元となる SKU と価格のデータベース(前回の講義でモデル化しました)。
- 簡単な注文モデル: テーブル orders(id、userId、skuId、amount、currency、status、createdAt)。
- ChatGPT App のインターフェースと、ギフトを推薦できる MCP レイヤー(前のモジュールで構築済み)。
この上に小さなサービス gg-acp を追加します。
- エンドポイント POST /checkout_sessions:
- SKU リストと数量を受け取る。
- 自社 DB に基づいて金額を再計算する。
- 下書き注文(例: ステータス pending)と、ステータス ready_for_payment の checkout_session を作成する。
- checkout_session を返す。
- エンドポイント POST /checkout_sessions/{id}:
- セッションと注文を見つける。
- 変更を適用(例: 合計を減らすプロモコードの適用)。
- 更新済みの checkout_session を返す。
- エンドポイント POST /checkout_sessions/{id}/complete:
- SPT、金額、購入者データを受け取る。
- デモ版では、PSP への実コールの代わりに注文を「支払い済み」にするだけでもよい(Stripe をシミュレートしてもよい)。
- checkout_session を completed に更新し、order_id を紐づける。
このサービス全体は小さな Node/Express アプリや Next.js App Router のエンドポイント群として実装可能です。たとえ決済をエミュレートしていても、形式とステータスのコントラクトを守ることが肝心です。
TypeScript での注文モデルの一例は次のとおりです。
// GiftGenius の簡易注文モデル
type GGOrderStatus = 'pending' | 'paid' | 'canceled';
type GGOrder = {
id: string;
userId: string;
skuId: string;
amount: number;
currency: 'usd';
status: GGOrderStatus;
};
本番では、チャットがどのユーザーであるかを知るための Auth/Identity 連携、OpenAI への webhook、より複雑な返品シナリオなどが追加されます。しかし学習の第一歩としては、「セッション作成 → 更新 → 完了」のループを確実に回し、お金と正気を失わないことができれば十分です。
10. ACP / Instant Checkout 設計での典型的な誤り
誤り #1: 役割の混同(「ChatGPT = 私のショップ」)。
開発者が ChatGPT を「基幹の記録システム」とみなして、注文のビジネス状態をプラットフォーム側に保持しようとすることがあります。「checkout_session があるのだから、注文履歴も OpenAI から読めばいい」といった発想です。これは行き止まりです。checkout_session はプロトコルのオブジェクトであり、注文の真実の所在ではありません。真実の所在はあなたのコマース・バックエンドであり、注文、ステータス、返品、レポートはそこに存在すべきです。ChatGPT は信頼できる「チャットのフロントエンド」にすぎません。
誤り #2: ChatGPT からの入力価格を信用してしまう。
「エージェントがすでに SKU を選び合計も計算した。ならその合計で請求すればいい」と考えがちですが、これは禁物です。ChatGPT からの入力(line items、想定価格)は提案に過ぎず、指示ではありません。あなたのバックエンドは、Product Feed と自社 DB に照らして SKU、価格、在庫、割引適用可否を自ら検証する義務があります。さもないと「モデルが丸めたせいで $0.01 で商品が買えてしまった」といった笑えないバグが発生します。
誤り #3: ステータスとステートマシンを軽視する。
初期プロトタイプでは「穴だらけ」の実装になりがちです。セッションのステータスが常に completed だったり、単に ok として実際の決済状態の不一致を内部で隠したり。結果として ChatGPT はユーザーに状況(支払い処理中か、完了か、キャンセルか)を適切に表示できません。not_ready_for_payment → ready_for_payment → completed/canceled のステートマシンを正直に実装し、バックエンドから実際のステータスを返すほうがずっと堅牢です。
誤り #4: Shared Payment Token を「使い回せるカード」のように扱う。
SPT は本来、1 回限りまたは厳格に制限されたトークンです。特定の取引・金額・マーチャントに結びついています。「念のため」にキャッシュしたり、別の購入で再利用したりするのは悪手です。良くて PSP に拒否され、悪ければ決済と注文の整合が崩れます。各 checkout_session.complete には新鮮なトークンが必要で、支払いに失敗したら再取得が必要です。
誤り #5: /checkout_sessions と webhook で冪等性を確保していない。
実ネットワークではリクエストが重複します。タイムアウト後に ChatGPT が POST /checkout_sessions を再送することもあれば、PSP が一時エラー後に webhook を再送することもあります。そのたびに新しい注文やレコードを作ってしまう実装だと、二重請求、注文の重複、システム間の不整合がすぐに発生します。Idempotency-Key の使用、重複検出、過去結果の保存は「任意の最適化」ではなく、堅牢な ACP 連携の必須要素です。
誤り #6: Product Feed との整合を忘れる。
ACP レイヤーを「真空中」で設計してしまうことがあります。SKU と価格を、Product Feed と一致しない内部テーブルから取ってくるようなケースです。結果として、ChatGPT はユーザーにフィード由来の情報を表示する一方、ACP の checkout ではまったく別の内容が通ってしまいます。こうした不意打ちを避けるには、SKU と価格のモデルを単一化することが重要です。Feed、ACP バックエンド、内部 DB は同じ真実の所在を参照し、上に異なるプロジェクションやキャッシュがあっても整合しているべきです。
GO TO FULL VERSION