1. 為什麼需要 ACP,且它為什麼不只是「另一個 REST API」
如果用比較犬儒的角度看,ACP 就像一組普通的 HTTP 端點與 JSON 結構:某個 /checkout_sessions、一些 webhooks、一些 token。很容易想:「OK,這又是某個平台的客製 API。」但 ACP 的想法更深。
ACP 被設計成在三方之間的開放式協定:AI 平台(例如 ChatGPT)、你的商家後端(commerce backend),以及支付服務提供商。它的目標是標準化:如何描述產品與價格、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 | 商品 feed 的格式與欄位(SKU、價格、可用性、連結、旗標)。 | 由 OpenAI 索引的禮物 JSON/CSV feed。 |
| Agentic Checkout | 針對 checkout_session 的 REST 合約:建立、更新、完成。 | 我們的 ACP 後端:/checkout_sessions 端點與 webhooks。 |
| Delegated Payment | 付款資料如何以委派的 token 形式傳遞給商家。 | 在完成付款時與 Stripe 的 Shared Payment Token 協作。 |
Product Feed 我們已在前一講解析。接下來關注後兩塊:Agentic Checkout 與 Delegated Payment。
重要的是劃分三個層級:
- 標準(SPEC)。 官方文件描述必備欄位與端點、哪些狀態合法,以及你所承諾的保證。
- 架構樣式(ARCH)。 例如把 SKU 與訂單放在獨立資料表、為 ACP 設計服務包裝層、或用佇列處理 webhooks。這些是良好實務,但不屬於標準本身。
- 具體實作(GiftGenius 範例)。 我們的教學專案:資料表結構、TypeScript 中精確的型別名稱、如何記錄訂單等。這些是示例,而非規範文件。
我們會不斷強調 SPEC 與你自家架構的分界——避免出現「我在講義裡看到欄位 persona_tags,就以為它是官方規範的一部分」這種誤解。
3. 從內部看 checkout_session:結構與狀態
Agentic Checkout 規範的核心物件,是你後端上的 checkout_session。邏輯上它代表一次購買的狀態:有哪些商品、金額多少、有哪些配送選項,以及目前付款嘗試所處的狀態。
規範大致這樣描述 checkout_session 的必要欄位(表述相對簡化,與原文略有出入):
- id — 由你產生並返回的字串識別碼。ChatGPT 會在之後的所有呼叫中使用它。
- buyer — 購買者資訊:姓名、email、電話,有時包含地址。在正式規範中,這個物件是結構化的,以便 PSP 與你的系統能可靠地使用。
- status — 反映當前購買狀態的字串 enum。基礎狀態:
- not_ready_for_payment — 尚未可付款(例如,未選擇配送方案或未重算稅金)。
- ready_for_payment — 一切就緒,可以請求付款 token 並扣款。
- completed — 付款成功,訂單已建立。
- canceled — 購買已取消(使用者主動或因錯誤)。
- currency — 以小寫表示的 ISO 4217 貨幣代碼("usd"、"eur" 等)。
- line_items — 購物車項目清單,每項包含 SKU、數量與計算後的金額。
- fulfillment_address — 配送地址(若相關)。
- fulfillment_options 與 fulfillment_option_id — 可用的配送(或履約)選項與當前選中的選項。
- totals — 聚合金額:商品金額、稅金、運費、總額。
- order — 描述成功完成會話後將建立之訂單的物件。
- messages — 可顯示給買家的訊息清單:例如警告或錯誤。
- 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 存在、可銷售、且不違反政策(例如未成年者不得購買酒精)。
- 依你的規則計算價格與稅金。
- 準備配送選項(若為實體商品)。
- 回傳正確的 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 的介面中輸入或選擇付款資料(卡片、錢包、已儲存的付款方式)。平台不會把這些資料直接傳給你——它會向 PSP(例如 Stripe)請求特殊 token:Shared Payment Token(SPT),它:
- 唯一綁定商家與特定會話;
- 對金額與有效期有限制;
- 不向你揭露真實卡號。
於是形成這樣的分工:
| 角色 | 能否看到卡片支付資訊 | 能否看到 Shared Payment Token | 能否看到訂單明細(SKU、金額) |
|---|---|---|---|
| 使用者 | 是(在 UI 中輸入) | 否(不需要) | 部分(買什麼與金額) |
| ChatGPT/OpenAI | 是(在付款流程中) | 是 | 是 |
| PSP (Stripe) | 是 | 是 | 在付款範圍內 |
| 商家 | 否 | 是 | 是 |
這種設計讓商家無須保存支付卡資料,專注於訂單業務邏輯,合規問題留給 PSP 與平台處理。
洞見
Shared Payment Token 的意義在於:不把卡片資料暴露給你的後端,但付款由你發動。也可以換個角度理解它。
你大概碰過這種情況:商店或飯店先在你的卡上做預授權(hold),之後再扣款。把 Shared Payment Token 當成 hold token:ChatGPT 已先在使用者帳上做了預授權,但尚未實際扣款。它把這個 hold token 交給你,你再把它轉交給 Stripe 完成扣款。
這裡有兩個關鍵細節:
- 預授權金額與實扣金額不應差太多,最好完全一致。
- 你可以透過 ChatGPT 以 $1 賣出首月訂閱,之後每月再扣 $49.99。
請求 POST /checkout_sessions/{id}/complete
當使用者在 Instant Checkout 中按下確認付款:
- ChatGPT 會向 PSP(例如透過 Stripe ACP API)請求 SPT。
- 接著它透過 POST /checkout_sessions/{id}/complete,把這個 token 與買家資料傳給你的後端。
規範大致這樣描述請求本文(以下為自官方文件調整縮寫的示例):
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"
}
}
你的後端應:
- 找到 id 為 checkout_session_123 的 checkout_session。
- 檢查當前狀態是否允許完成(通常為 ready_for_payment)。
- 在 PSP 建立付款,使用 spt_123(方式取決於 PSP;以 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);
});
在真實世界中,這幾行之間還會有錯誤處理、重試、記錄與你自家訂單模型的整合。
若發生問題(例如付款被拒),你應回傳狀態為 not_ready_for_payment 或 canceled 的 checkout_session,並填好 messages,讓 ChatGPT 能正確向使用者解釋發生了什麼事。
8. ChatGPT 的 Instant Checkout:如何串成一條流程
現在把這些拼成在 ChatGPT 中「從意圖到付款」的完整情境。可以把本講視為對小工具上那顆「購買」按鈕背後機制的解碼。
簡化劇本:
- 使用者輸入:「幫我挑一個給朋友的數位禮物,預算不超過 $50,並直接完成購買」。
- 代理(或 ChatGPT App)使用 Product Feed 在預算內尋找合適的 SKU。
- ChatGPT 透過你的 GiftGenius 小工具在對話裡顯示多張禮物卡片,並請使用者選擇其一。
- 選定後,ChatGPT 形成 line items 並呼叫 POST /checkout_sessions 到你的 ACP 後端,取得包含金額與狀態的 checkout_session。
- 在 Instant Checkout 的 UI 中,使用者會看到總額、商品名稱、退貨政策與確認按鈕。
- 點擊確認時,ChatGPT 向 PSP 取得 Shared Payment Token,並呼叫 POST /checkout_sessions/{id}/complete,如前所述。
- 你的後端完成付款、建立訂單,並回傳狀態為 completed 的 checkout_session。
- ChatGPT 向使用者顯示確認,而你的後端(依 Agentic Checkout 規範的 webhooks)可以把事件回推給 OpenAI,讓平台知道訂單的最終狀態。
用 sequence 圖表示如下:
sequenceDiagram
actor U as 使用者
participant GPT as ChatGPT
participant GG as GiftGenius ACP backend
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: 顯示購買確認
在此流程中,不需要任何「任意」呼叫你的資料庫或奇怪的內部端點。一切都落在 ACP 嚴謹描述的合約中,每個參與者都清楚自己的角色。
9. 小實作:GiftGenius 的簡化版 ACP 後端
為避免本講停留在純理論,重要的是在腦中「走一遍」我們教學專案的 ACP 層實作。
假設 GiftGenius 已經有:
- SKU 與價格資料庫,據此我們產生 Product Feed(前幾講已建模)。
- 簡單的訂單模型:資料表 orders,欄位 id、userId、skuId、amount、currency、status、createdAt。
- ChatGPT App 介面與 MCP 層,能推薦禮物(我們在先前模組中已建立)。
現在你的任務——再加上一個小服務 gg-acp:
- 端點 POST /checkout_sessions:
- 接收 SKU 清單與數量。
- 依你的資料庫重算金額。
- 建立草稿訂單(例如狀態 pending)與狀態為 ready_for_payment 的 checkout_session。
- 回傳 checkout_session。
- 端點 POST /checkout_sessions/{id}:
- 找到會話與訂單。
- 套用變更(例如支援折扣碼,降低總額)。
- 回傳更新後的 checkout_session。
- 端點 POST /checkout_sessions/{id}/complete:
- 取得 SPT、金額與買家資料。
- 在 demo 版中,可以不實際呼叫 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 的 webhooks,以及更複雜的退款情境。但作為本講的學習步驟,能夠穩健地跑一圈:建立會話 → 更新 → 完成,同時不丟錢也不失理性,就足夠了。
10. ACP / Instant Checkout 設計中的常見錯誤
錯誤 #1:角色混淆(「ChatGPT 就是我的商店」)。
有時開發者把 ChatGPT 當成「核心記帳系統」,試圖把訂單的業務狀態放在平台端:「既然有 checkout_session,那我就從 OpenAI 讀訂單歷史吧」。這是死路。checkout_session 是協定物件,不是訂單的真實來源。真實來源是你的商務後端:訂單、狀態、退款與報表都應該在那裡。ChatGPT 在此架構中只是可信的「對話前端」。
錯誤 #2:信任來自 ChatGPT 的輸入價格。
很容易想:「代理已選好 SKU 還算出總額,就照單全收吧。」不可以。來自 ChatGPT 的輸入(line items、預估價格)只能視為建議,而非命令。你的後端必須自行檢查 SKU、價格、可用性、折扣適用性等,並與 Product Feed 與自家資料庫比對。否則你可能遇到各種有趣的錯誤:「使用者以 $0.01 買到商品,因為模型決定做了四捨五入」。
錯誤 #3:忽略狀態與狀態機。
早期原型常見「漏水」實作:會話狀態永遠是 completed,或只是 ok,而真實的付款狀態差異被藏在內部。結果 ChatGPT 無法正確地向使用者呈現正在發生的事:付款還在進行、已完成或已取消。更可靠的做法是誠實實作狀態機 not_ready_for_payment → ready_for_payment → completed/canceled,並由後端回傳真實狀態,而不是發明臨時欄位。
錯誤 #4:把 Shared Payment Token 當「可重複使用的卡」。
依設計,SPT 是一次性或嚴格受限的 token:綁定特定操作、金額與商家。試圖快取它「以備不時之需」或重用於其他購買,是糟糕的主意。最好的情況是 PSP 直接拒絕第二次嘗試;最糟的是你把付款與訂單的記錄搞亂。每次 checkout_session.complete 都應該有全新的 token;若付款未成功,需重新取得新的 token。
錯誤 #5:在 /checkout_sessions 與 webhooks 中缺乏冪等性。
在真實網路中請求會被重送:ChatGPT 可能在逾時後重送 POST /checkout_sessions,PSP 也可能在暫時性錯誤後重送 webhook。若你的實作每次都建立新的訂單與資料列,很快就會一團混亂:重複扣款、重複訂單,以及系統間的奇怪差異。使用 Idempotency-Key、檢查重送、保存前一次呼叫的結果——這不是「可選的最佳化」,而是打造穩健 ACP 整合的必要元素。
錯誤 #6:忘了與 Product Feed 對齊。
有時 ACP 層被設計在「真空」裡:SKU 與價格取自某些內部資料表,卻與最終進入 Product Feed 的內容不一致。結果 ChatGPT 給使用者看的(依 feed)與透過 ACP 結帳的內容完全對不上。要避免這類情況,你的 SKU 與價格模型務必一致:feed、ACP 後端與內部資料庫應該指向同一真實來源,即使表層有不同投影與快取。
GO TO FULL VERSION