1. 为什么需要 ACP,它为何不只是“又一个 REST API”
如果从挑剔的角度看,ACP 像是一组普通的 HTTP 端点与 JSON 结构:某个 /checkout_sessions、一些 webhook、一些 token。很容易觉得:“好吧,这又是某个平台的定制 API。”但 ACP 的思想更深。
ACP 被设计为三方之间的开放交互协议:AI 平台(例如 ChatGPT)、你的电商后端以及支付服务提供商(PSP)。它的目标是标准化:如何描述商品与价格、AI 如何声明用户的购买意图、如何创建结账会话、如何执行支付,以及所有参与者如何获知最终状态。
关键点在于:同一个实现了 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 端点以及 webhook。 |
| Delegated Payment | 如何以委托 token 的形式把支付数据传递给商户。 | 在完成支付时与 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 —— 你生成并返回的字符串会话标识。ChatGPT 将在后续调用中使用它。
- buyer —— 买家信息:姓名、邮箱、电话,有时还有地址。在正式规范中,该对象有明确结构,以便 PSP 与你的系统可靠使用。
- status —— 反映购买当前状态的字符串枚举。基础状态:
- 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 —— 给用户展示的消息列表(如提醒或错误),ChatGPT 会使用它们与买家沟通。
- links —— 链接列表,例如退货政策、隐私政策与服务条款(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)。该 token:
- 与商户及该会话唯一关联;
- 对金额与有效期受限;
- 不会向你暴露真实卡号。
结果是这样的分工:
| 参与方 | 能看到卡的支付信息 | 能看到 Shared Payment Token | 能看到订单明细(SKU、金额) |
|---|---|---|---|
| 用户 | 是(在 UI 中输入) | 否(不需要) | 部分(买了什么、多少钱) |
| ChatGPT/OpenAI | 是(支付流程中) | 是 | 是 |
| PSP(Stripe) | 是 | 是 | 在支付上下文内 |
| 商户 | 否 | 是 | 是 |
这种设计让商户无需保存支付卡信息,专注于订单业务逻辑,把合规问题交给 PSP 与平台。
洞见
Shared Payment Token 的意义在于:对你的后端隐藏卡信息,但由你来发起支付。也可以换个角度理解它。
你或许遇到过这样的场景:商店或酒店先在你的卡上做预授权(hold),随后再扣款。可以把 Shared Payment Token 当作预授权 token。ChatGPT 已在用户账户上做了预授权,但未扣款。它把这个预授权 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)。
- 使用 spt_123 在 PSP 侧创建支付(具体取决于 PSP;以 Stripe 为例,会有相应的端点与 payment method 类型)。
- 等待支付操作确认。
- 将 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 并调用你的 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,让平台得知订单的后续。
用 sequence 图表示如下:
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: 向 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、金额与买家信息。
- 在演示版中可以不真正对接 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、预估价格)只能视作建议,而非命令。你的后端必须自行校验 SKU、价格、库存、折扣适用性等,并与 Product Feed 与自有数据库对照。否则你会遇到“模型四舍五入出 bug,用户 $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;若支付失败,就需要重新申请。
错误 5:在 /checkout_sessions 与 webhook 中缺少幂等性。
真实网络里请求可能被重复: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