payin商户操作手册

最新文章

Stripe 零金额 Checkout:没有 PaymentIntent 时如何履约

以 Checkout Session 驱动零金额订单履约,区分完成状态与支付状态,处理空支付引用、重复交付与排查边界。

成稿与来源核验:

覆盖一次性托管结账中的免费商品和全额折扣订单;不覆盖订阅生命周期、税务建议、会话过期或库存预留。所查来源不能证明完整地区适用范围。

订单可以成立,支付对象却不存在

零金额 Checkout 订单,不是等待补齐 PaymentIntent 的失败扣款。Stripe 官方明确说明:已完成的零金额 Checkout Session 没有关联的 PaymentIntent,履约应处理 checkout.session.completed,而不是依赖 PaymentIntent 事件。[1] 如果系统一进入处理函数就要求存在 pi_ 标识,免费课程、兑换商品或下载权限可能在没有明显报错的情况下被跳过。修复方向不是伪造付款记录,而是以 Checkout Session 作为订单履约入口,让支付引用成为可选关联。

本文仅讨论 mode=payment 的一次性托管 Checkout,包括零价格商品,以及折扣后最终总额归零的订单。重点是“有已完成订单、无支付对象”的履约分支,不讨论会话过期、库存释放或支付链接完成次数限制。订阅试用、setup 模式收集支付方式、税务处理和具体账户资格需要单独判断。所查官方材料没有提供国家或地区逐项支持名单,因此不能把文档中的示例理解为所有地区、所有账户都适用。[1]

先确认官方保证了什么

官方要求使用 2023-08-16 或更高的接口版本处理 Checkout Sessions API 零金额订单。可以创建 unit_amount=0 的 Price,也可以通过 price_data 传入免费价格;百分之百优惠券也是明确列出的实现路径。订单总额为零时,Checkout 不收集支付方式。如果未指定 customer,这一流程会自动创建 Customer,因此不支持访客客户模式。[1] “不用付款”不等于“不用维护客户归属”,本地用户与客户对象的关联仍需设计。

尤其不要把一个脆弱假设换成另一个:不能仅凭社区个案,就断言所有零金额订单的 payment_status 必然是 no_payment_required。官方对象参考列出 paid、unpaid 和 no_payment_required,官方履约示例则使用“不是 unpaid”作为状态条件。[2][3] 本次读取的对象参考在 no_payment_required 下描述的是 setup 模式,以及账单周期锚点到来前暂不收款的情形;paid 的描述还包含已成功处理的零金额订阅试用账单。[3] 因此,本地测试应记录实际响应、固定相关接口版本,而不是把某篇社区文章中的字段组合当成所有产品模式的统一契约。确定无疑的核心事实,是官方明确说零金额已完成会话没有 PaymentIntent。[1]

把履约写成明确的业务决策

  1. 接收 checkout.session.completed,并验证事件签名。把其中的 Session ID 交给统一的服务端履约函数。官方指南也把 checkout.session.async_payment_succeeded 交给同一函数,用于延迟支付之后的成功处理。[2]
  2. 重新读取 Session,并展开 line_items;商品很多时继续读取分页。官方要求使用最终完成的商品及数量,而不是只依赖进入 Checkout 前的购物车快照。[1][2] 作为应用层控制,还应匹配本地订单、账户上下文、客户归属、运行环境和 mode,避免只拿到一个会话标识就授予任意权益。
  3. 对于本文的一次性订单处理函数,确认会话已经完成,再检查 payment_status。对象参考明确指出 status=complete 时支付处理仍可能进行中,不能仅凭 complete 就发货。[3] unpaid 不进入即时交付;paid 和 no_payment_required 可以作为允许继续判断的支付状态,但不能替代商品、身份和完成状态检查。
  4. 若 amount_total 为零,且其他履约条件成立,就允许 payment_intent 为空。若总额非零却出现不符合预期的空引用,应继续调查状态与集成,不要自动把订单改成免费。单独一个空字段既不能证明订单免费,也不能证明订单已完成。
  5. 按 Session 和本地订单持久化履约结果,并设置能抵抗并发的唯一性约束。官方提醒同一会话的履约函数可能被多次甚至同时调用。[2] 数据库事务、持久任务队列或事务外发箱属于建议的应用实现方式,不是“收到 webhook 就自动保证只履约一次”。

三个容易漏掉的实际场景

场景一:原本收费的课程用了全额优惠券。用户仍然应获得课程权限,但本地数据库把 payment_intent_id 设为非空,导致权限事务无法提交。可把支付关联改为可选,并单独记录“零金额完成”的结算分类;保留真实金额、币种、折扣信息和最终商品明细。不要为了兼容旧表结构,写入虚假的扣款成功或支付标识。免费订单是业务订单,不是伪装成收费交易的空壳。

场景二:免费实物仍然需要配送。应用要明确收货信息从哪里取得,后续运营任务如何产生。判断时使用最终总额,而不是看到“商品价格为零”或“百分之百折扣”便提前进入免费分支。商品免费并不能独立证明整笔订单总额为零;官方不收集支付方式的条件是总额归零。[1] 配送或其他金额是否存在,应以服务端读取到的最终会话为准。

场景三:用户回到成功页时,webhook 也正在处理。官方推荐使用 webhook,并允许成功页调用同一履约函数改善即时体验,同时提醒不能保证每个客户都会到达成功页。[2] 两条路径必须共用同一个去重边界。成功页地址不是收款凭证,也不是授予浏览器参数所指定商品的授权;页面刷新不能再次创建权益或重复发货。

按症状排查,而不是等待不存在的事件

  1. 没有发放权益:先核对是否存在已完成的会话,再确认端点是否接收 checkout.session.completed。如果只订阅 payment_intent.succeeded,官方定义的零金额订单就没有这条触发路径。[1]
  2. 事件到了却没有动作:检查是否存在“没有 payment_intent 就返回”或“只接受单一 payment_status”的提前退出条件。建议记录会话标识、模式、最终总额、支付状态和履约决策理由,不要为了排查而完整输出不必要的客户资料。
  3. 程序进入分支但保存失败:检查非空支付字段、强制关联支付表的查询、收据模板,以及下游直接读取支付对象属性的任务。把货币退款与免费权益撤销分开建模;缺少 PaymentIntent 不代表应该补造一次扣款。取消免费订单可能仍有业务操作,但不是凭空创造退款所需的交易。
  4. 上线前在沙箱覆盖零价格商品、全额折扣、普通付费订单和仍为 unpaid 的延迟支付完成事件。再重复投递事件,并发调用相同履约函数,确认符合条件的订单只产生一次业务交付。这些是建议执行的验收项目,本文没有运行真实结账,也没有宣称这些测试已通过。

产品边界、地区与资料日期

如果入口是 Payment Links 或价格表,还要查看额外的账户创建时间和启用条件。零金额指南说明,2023 年 8 月 17 日之后创建的账户默认支持这些入口的零金额订单;更早的账户可在 Checkout 设置中启用,并有三天可再次关闭的宽限期。[1] 这是启用条件提醒,不是支付链接次数限制教程。修改正式账户前应阅读当前指南,不要把一次性免费订单的规则直接用于订阅试用或未来扣款安排。

来源抓取日期为 2026 年 9 月 22 日。所读取材料未注明发布日期或最后更新日期,不能把抓取日期当成发布日。文中的接口版本日期、账户创建日期是产品条件,不是来源发布日期。官方材料未给出完整地区支持清单,实际适用性仍须核对账户所在地及对应产品条件。本研究只访问公开文档和搜索结果,没有读取凭据、登录 Stripe 账户或运行真实支付。

来源与日期

来源为官方文档,站点可能返回本地化语言文本;未注明发布或更新日期,于2026-09-22读取核验。检索日期不等于原文发布日期。本文为文档研究,不是实际账户测试,不代表 PayIn 产品功能,也不是法律、税务或财务意见。

延伸阅读

全部指南