跳至正文
系统集成 — Stripe 接入指南:角色、对象、Checkout 与 Webhook

Stripe 接入指南:角色、对象、Checkout 与 Webhook

AI 参与说明(Agent:Grok):本文由 Grok 根据 Stripe 官方文档与 Context7 检索结果整理并校验,资料整理于 2026-09-08。运行记录:模型 grok-4.6,提供方 xAI,执行入口 Grok Build TUI。CLI 版本与 reasoning effort 未取得运行记录。正文只列出关键 SDK 方法名称与输入输出,不构成可直接上线的完整实现;账户开通、费率、支持国家、税务责任与产品可用性以 Stripe 当前官方页面为准。

修订说明(2026-09-16,Agent:Codex):依据 Stripe 与 PayPal 官方文档补充两者关系、PayPal 独立接入、商家地区限制及结算方式,并区分标准集成与需申请访问的 PayPal custom payment method。运行记录:模型 gpt-6-astra,reasoning effort ultra,提供方 openai,执行入口 Codex Desktop,CLI 版本 0.154.0-alpha.6.2(不代表桌面 App 版本)。本次核验为文档与页面校验,未操作真实支付账户或执行交易。

导航补充(2026-09-16,Agent:Codex):增加通用支付角色与生命周期文章的前置阅读入口。模型 gpt-6-astra,reasoning effort medium,提供方 openai,执行入口 Codex Desktop,CLI 版本 0.154.0-alpha.6.2。

修订说明(2026-09-17,Agent:Codex):补充 AI credits 订阅与一次性 / 周期 add-on 的商品建模、Workbench 配置、事件清单、幂等履约、额度账本、退款与验收步骤。通过 Context7 与 Stripe CLI 文档读取核对官方资料;额度有效期、升级补差和退款政策均为示例工程决策。模型 gpt-6-astra,reasoning effort medium,提供方 openai,执行入口 Codex Desktop,CLI 版本 0.154.0-alpha.6.2(不代表桌面 App 版本)。未操作真实支付账户或执行端到端交易。同日继续将第十二节展开为 Dashboard / API 退款发起、Refund Webhook 状态同步、额度冲正与订阅取消的完整流程。

Stripe 把一次收款拆成一组会随时间变化的对象,而不是一次函数调用就结束。第一次接入,先认清谁在付钱、谁在收款、谁在记账,以及哪一步才真正授权业务权限。官方对大多数在线收款的建议是先用 Checkout Sessions API;自定义表单和底层 PaymentIntent 可以后补。Online payments

如果还不熟悉 Payment Gateway、PSP、Acquirer 和 Issuer,先读 支付业务角色与概念,再把通用概念映射到本文的 Stripe 对象。

正在接入 AI credits 订阅 + add-on 时,先读第七节的商品与事件清单,再按第八节完成 Dashboard 配置、额度履约与测试。本文以一次性额度包为主,也单独说明每期续费的 add-on;两者不能用同一套发放假设。

如果只想弄清 Stripe 与 PayPal 的关系,先看下文「Stripe 与 PayPal:可以一起用,也可以独立接入」。

如果目标是由第三方作为转售方处理消费者销售税,站内已有 Polar 的接入说明;本文讨论的是直接接入 Stripe 时的对象模型与流程。两种路径都依赖 Webhook 同步状态,成功页本身不能授予权限。

阅读前先看这几个词

下面的名称贯穿全文。中文名称只用于阅读对照,后文固定使用英文名称。

英文术语中文名称简要解释
Payment Processor支付处理方帮商家向银行和卡组织收款、记账并结算的服务。Stripe 默认扮演这个角色
Merchant of Record销售记录商家对买家开票、承担消费者销售税的那一方。直接接入 Stripe 时,通常是你自己
Customer客户对象Stripe 里代表付款人的记录,用来保存邮箱、支付方式和订阅
PaymentMethod支付方式卡、钱包或其他付款凭证的引用,不是卡号明文
PayPalPayPal既向消费者提供付款钱包,也向商家提供可独立接入的收款服务
PayPal custom payment methodPayPal 自定义支付方式在 Stripe 结账界面展示 PayPal,再经商家部署的适配器处理 PayPal 交易的接入路径
PaymentIntent收款意图跟踪“准备收一笔钱”的全过程,记录尝试、认证和最终结果
Charge扣款尝试一次具体的资金划转尝试;失败后可以在同一 PaymentIntent 上重试
Checkout Session结账会话一次具体结账流程的服务端对象,完成后返回托管页面或嵌入式表单
Checkout结账产品Stripe 提供的预构建结账界面,由 Checkout Session 驱动
Payment Link付款链接可分享的结账入口,打开后仍会创建 Checkout Session
Product产品卖什么,例如 Pro Monthly
Price价格金额、币种和是否按周期计费
Invoice账单订阅或一次性开票时生成的应收记录
Subscription订阅客户与 recurring Price 的持续计费关系
Event事件Stripe 对账户里发生事情的通知记录
Webhook事件投递把 Event 推到你的 HTTPS 端点,用来驱动履约
Publishable key可发布密钥可以放进前端,只能创建令牌或 PaymentMethod,不能扣款
Secret key密钥后端凭据,能创建收款、读账户数据;不要进前端或 Git
Restricted API key受限密钥按权限裁剪的后端密钥,官方更推荐新集成使用它
Client secret客户端密钥单个 PaymentIntent 或 Checkout Session 的短期凭证,只用于这次结账
Stripe.js / Elements前端收款组件在浏览器里收集支付信息并交给 Stripe,避免卡号经过你的服务器
Billing Portal客户账单门户让客户自己改支付方式、看发票、取消订阅的托管页面
Balance余额已收款、待结算、可打款的资金视图
Payout打款把 Balance 转到商家银行账户
Refund退款把已收资金退回客户
Dispute拒付争议客户向发卡行提出争议后的处理流程
Connect平台收款让平台或市场在多个 Connected Account 之间分账
PCI DSS支付卡行业数据安全标准处理卡数据时必须遵守的安全基线
3D Secure持卡人认证部分卡交易要求客户额外确认身份,常见于欧洲 SCA

一、Stripe 负责什么,应用负责什么

Stripe is a Payment Processor: it collects payment details, talks to card networks and banks, tracks PaymentIntent status, and later sends a Payout to the merchant bank account. The business that signs up for Stripe is usually the Merchant of Record, so tax, refunds, disputes, and KYC remain the merchant’s responsibility unless a separate Stripe product explicitly changes that split. Tour of the API、Activate your account

应用仍然负责自己的用户身份、商品目录映射、履约和功能授权。Stripe 确认“钱到了”,不等于应用已经开权限;应用开了权限,也不等于 Stripe 已经完成结算打款。

这张图回答“一次收款里有哪些角色”。箭头表示责任,不表示网络包的到达顺序:

flowchart TB
    Buyer["付款人"] --> App["应用后端"]
    App --> Stripe["Stripe"]
    Buyer --> Stripe
    Stripe --> Networks["卡组织 / 银行"]
    Stripe --> MerchantBank["商家银行账户"]
    App --> Entitlements["应用内权限与履约"]
    Stripe --> App

和 Polar 对比时,差别主要在销售主体,不在“要不要做 Webhook”:

问题直接接入 StripePolar
谁对买家开票通常是商家自己Polar 作为 Merchant of Record
消费者销售税商家自己处理,或另接 Stripe Tax 等产品Polar 处理其服务范围内的销售税
第一次实现Checkout Session + WebhookCheckout + Webhook + Customer State
钱如何到商家Balance 再 Payout 到银行Polar 的 Payout Account,底层走 Stripe Connect Express

费率、开通国家和税务边界都会变,不要把这张表当成报价单。需要 MoR 路径时,看 Polar 接入指南。

Stripe 与 PayPal:可以一起用,也可以独立接入

Stripe 与 PayPal 是不同公司的支付服务,彼此没有必须搭配使用的依赖关系。 从商家接入的角度,它们都能提供收款能力;从消费者结账的角度,PayPal 又可以成为 Stripe 支持的一种支付方式。因此,「通过 Stripe 提供 PayPal 支付」和「直接接入 PayPal」都成立。Stripe PayPal payments、PayPal Checkout

接入路径网站接入什么需要区分的边界
Stripe 标准 PayPal 集成Stripe Checkout、Elements 或 Payment Links 中启用 PayPal商家 Stripe 账户必须位于支持地区,正式启用时关联 PayPal 账户
独立接入 PayPal直接使用 PayPal Checkout 与 PayPal API不需要 Stripe;资格、支付方式与结算由 PayPal 的对应产品规则决定
网站同时接入两家例如银行卡走 Stripe,PayPal 按钮走独立 PayPal 集成两套交易来源;应用需分别处理支付确认、退款和对账

第三行是前两种独立能力组合后的工程方案,不表示在 Stripe Dashboard 打开一个开关就自动完成双渠道集成。

标准集成有商家地区限制。 截至 2026-09-16,Stripe 的启用文档列出:欧盟国家(匈牙利除外)、列支敦士登、挪威、英国和瑞士。美国、香港及中国大陆不在这份标准集成名单中。这里看的是商家 Stripe 账户所属地区,不是买家所在地,也不是结账币种;支持 USD 或香港买家付款,不能推导出美国或香港商家账户能直接启用。Activate PayPal payments

符合条件的商家,在 Stripe Dashboard 的 Payment methods 中找到 PayPal 并启用,选择结算方式,再转到 PayPal 关联现有账户或创建新账户。完成关联后还要确认状态已可用;待验证状态不代表已经能正式收款。Activate PayPal payments

Integration choice and settlement destination are separate decisions. 对直接经营业务的商家,标准集成可以选择将 PayPal 收款转入 Stripe Balance,或留在 PayPal 余额中并由 PayPal 管理提现。因而,「代码接的是 Stripe」不能单独说明「钱一定留在 Stripe」。Stripe Connect 场景另有约束,不能照搬普通商家的选择。Choose settlement preference

独立接入 PayPal 同样是完整路径。 PayPal Checkout 可在网站展示 PayPal 按钮,由服务端创建订单并完成收款;PayPal 还提供自己的 Subscriptions API 处理周期付费,不要求先拥有 Stripe 集成。信用卡等额外付款选项是否可用,仍要核对具体地区与商家资格。PayPal Checkout、PayPal Subscriptions

还有一种应单独理解的 PayPal custom payment method:商家使用自己的 PayPal 账户,在自己的环境部署 Stripe 编写的 PayPal adapter,由 Stripe 的结账界面将 PayPal 付款路由到该适配器。它并不等于上述标准 PayPal 集成。当前文档要求申请访问,并明确说明除了 PayPal 费用外还会收取 Stripe 费用,价格需联系 Stripe;不能把它理解为所有地区账户已默认开放的免费选项。PayPal custom payment method

选择时可以按以下顺序判断:

  1. 已有 Stripe,想增加 PayPal:先查商家账户地区与 Dashboard 中的可用状态;符合标准集成条件时,优先评估在现有 Stripe 流程中启用。
  2. 只需要 PayPal 收款:直接评估 PayPal Checkout;需要订阅时再核对 PayPal Subscriptions,无须为了 PayPal 先接 Stripe。
  3. 需要两家,但标准集成不支持:可以分别接入;若希望继续使用 Stripe 结账界面,再评估 PayPal custom payment method 的访问资格、适配器部署及额外费用。

以上是基于官方能力整理的选择建议,并非对特定账户的开通或交易验收结论。真正接入时,应在相应测试环境验证付款成功、取消、退款及服务端支付确认,再依据商家主体与账户审核结果判断能否上线。

二、先认清基础对象

Stripe 的 API 把账户里几乎所有东西都做成对象。Dashboard 里手动新建 Product,同样会生成 Product 对象。一次支付会同时用到多个对象,它们分工不同,不能互相替代。Tour of the API

flowchart TB
    Product["Product"] --> Price["Price"]
    Customer["Customer"] --> PaymentMethod["PaymentMethod"]
    Price --> CheckoutSession["Checkout Session"]
    Customer --> CheckoutSession
    CheckoutSession --> PaymentIntent["PaymentIntent"]
    CheckoutSession --> Subscription["Subscription"]
    PaymentIntent --> Charge["Charge"]
    Subscription --> Invoice["Invoice"]
    Invoice --> PaymentIntent
    PaymentIntent --> Event["Event"]
    Subscription --> Event
    Event --> Webhook["Webhook"]
    Charge --> Balance["Balance"]
    Balance --> Payout["Payout"]
对象表示什么接入时如何使用
Customer付款人在 Stripe 中的档案用自己的 user ID 写入 metadata,不要只靠邮箱猜归属
PaymentMethod可重复使用的付款凭证引用由 Stripe.js / Checkout 收集;服务端只保存 ID
PaymentIntent收一笔钱的状态机需要实际付款的 Checkout 流程通常由它跟踪;不要假设零金额场景必定存在
Charge某一次扣款尝试同一 PaymentIntent 可以有多次失败 Charge
Checkout Session一次结账流程后端创建后把 url 交给浏览器,或把 client_secret 交给嵌入式表单
Payment Link可重复分享的入口适合官网或邮件;每次访问仍会落到 Checkout Session
Product / Price卖什么 / 怎么计价Dashboard 先建好,后端只传允许出售的 Price ID
Invoice本期该收多少订阅续费时由 Stripe 自动生成
Subscription持续计费关系一个 Subscription 会产生多张 Invoice
Event“发生了什么”Webhook 投递的就是它
Balance / Payout已收款与打款和客户刷卡不是同一条链路

金额一律按币种的最小单位传递。1000 在 USD 表示 10.00 美元;JPY 等 zero-decimal 币种则 500 表示 500 日元。不要在业务代码里默认“所有金额都要乘 100”。Supported currencies

三、钱是怎么走的

下面以银行卡支付为例。付款人点“购买”时,应用还没有收到钱。Stripe 先记录一笔 PaymentIntent,再收集 PaymentMethod、确认、必要时做 3D Secure,最后才出现成功或失败。卡组织批准之后,资金进入 Stripe Balance,再按账户设置 Payout 到银行。这三件事时间不同,状态也不同。PayPal 的结算去向另见前文,不能把这条银行卡路径套到所有支付方式上。

sequenceDiagram
    participant Buyer as 付款人
    participant App as 应用后端
    participant Stripe as Stripe
    participant Bank as 卡组织 / 银行

    Buyer->>App: 选择商品并结账
    App->>Stripe: 创建 Checkout Session 或 PaymentIntent
    Stripe-->>App: 返回 url 或 client_secret
    App-->>Buyer: 打开 Checkout 或支付表单
    Buyer->>Stripe: 提交 PaymentMethod
    Stripe->>Bank: 授权与清算
    Bank-->>Stripe: 成功或失败
    Stripe-->>App: Event / Webhook
    App->>App: 履约或拒绝授权
    Note over Stripe,Bank: 随后才是 Balance 与 Payout

因此,业务上要分开三个问题:

  1. 这次结账有没有成功? 看 Checkout Session 的 payment_status,或 PaymentIntent 的 status。
  2. 应用该不该给权限? 由 Webhook 驱动的履约函数决定,并以自己的数据库为准。
  3. 商家什么时候能提现? 看 Balance 和 Payout,不参与“开会员”判断。

四、第一次接入该选哪条界面

官方把在线收款界面分成几档。复杂度从低到高,不是“越底层越专业”。Online payments

flowchart TB
    Start["要收一笔在线付款"] --> HasSite{"有自己的网站或应用吗"}
    HasSite -->|"没有,只要一个链接"| PaymentLink["Payment Link"]
    HasSite -->|"有"| WantCustom{"需要完全自绘结账页吗"}
    WantCustom -->|"不需要"| HostedCheckout["Hosted Checkout"]
    WantCustom -->|"需要"| OwnLogic{"还要自己实现税、折扣、运费吗"}
    OwnLogic -->|"不要"| CheckoutSessions["Checkout Sessions API + Elements"]
    OwnLogic -->|"要"| PaymentIntents["Payment Intents API"]
方案适用后端创建什么前端做什么
Payment Link没有完整网站,或运营人员要分享链接Dashboard 创建 Payment Link打开链接
Hosted Checkout大多数 SaaS 和第一次接入checkout.sessions.create,mode 为 payment 或 subscription跳转到返回的 url
Checkout Sessions + Elements想把表单嵌进自己的页面,但仍复用税、折扣、订阅同样创建 Checkout Session,使用嵌入式 ui_mode用 Stripe.js 渲染表单
Payment Intents API必须自己掌握结账状态机paymentIntents.create用 client_secret 确认支付

We recommend the Checkout Sessions API for most integrations. Payment Intents is the lower-level API; if you use it, you must rebuild tax, discounts, shipping, subscriptions, and currency conversion yourself. Online payments

不要一上来就在自己的 HTML 里放卡号输入框。PCI DSS 要求处理卡数据的各方都合规;把卡号直接发到自己的服务器,会把义务从“使用 Stripe 托管表单”变成完整的卡数据存储与传输控制。低风险做法是让 Stripe.js / Checkout / Elements 把敏感数据直接交给 Stripe。Integration security guide

五、一次性收款:Checkout Session 在做什么

一次 Hosted Checkout 的最小闭环是:后端创建 Checkout Session,浏览器打开 url,客户付款,Stripe 发 Webhook,应用履约,浏览器可能再回到 success_url。后两步没有固定先后。

sequenceDiagram
    participant Buyer as 付款人
    participant App as 应用后端
    participant Stripe as Stripe

    Buyer->>App: 已登录后点击购买
    App->>App: 从登录会话确定 Customer 映射
    App->>Stripe: checkout.sessions.create
    Stripe-->>App: Checkout Session.url
    App-->>Buyer: 302 到 Checkout
    Buyer->>Stripe: 完成付款
    Stripe-->>App: checkout.session.completed
    App->>App: fulfill_checkout 并记录幂等结果
    Stripe-->>Buyer: 跳转 success_url
    Buyer->>App: 成功页查询当前权限

checkout.sessions.create 的关键输入与输出:

方向字段含义
输入modepayment 一次性;subscription 订阅;setup 只保存支付方式
输入line_itemsPrice ID 与数量,或内联 price_data
输入success_url / cancel_url浏览器回跳地址,不是履约开关
输入customer已有 Customer ID;首次可以让 Stripe 按需创建
输入client_reference_id / metadata绑定你的订单号或 user ID,须来自已验证会话
输出id例如 cs_test_...,履约函数用它回查
输出urlHosted Checkout 的跳转地址
输出payment_status创建时通常是 unpaid
输出client_secret嵌入式 Checkout 才需要把它交给前端

You can’t rely on triggering fulfillment only from your checkout landing page. 客户可能付款成功后断网,永远看不到成功页;延迟到账的支付方式更不会在回跳当时变成最终成功。自动履约必须走 Webhook。Fulfill orders

官方履约函数的形态可以记成 fulfill_checkout(session_id):

  1. 对同一个 Checkout Session ID 可重入,并发调用也不能重复发货。
  2. 用 checkout.sessions.retrieve(session_id, { expand: ['line_items'] }) 回查最新对象。
  3. 检查 payment_status,不要只看见 Event 类型就发货。
  4. 履约后把“已经处理过这个 Session”写进自己的数据库。

一次性收款通常监听:

Event适合做什么不能推断什么
checkout.session.completed即时支付方式下开始履约不代表所有支付方式都已经到账
checkout.session.async_payment_succeeded银行转账等延迟成功后履约不能在 completed 时提前发货
checkout.session.async_payment_failed通知客户并取消待发货订单不代表 Checkout Session 从未被打开

测试卡常用 4242 4242 4242 4242,过期日用未来日期,CVC 用任意三位。这只在 Sandbox 有效。Testing

六、PaymentIntent:即便不用底层 API,也要认识这台状态机

A PaymentIntent tracks a payment lifecycle. 它表示“打算收一笔钱”,并跟踪这笔钱走到哪一步;免费试用或无须支付的零金额 Checkout 不应被假定一定存在 PaymentIntent。客户可能中途放弃,也可能换卡重试;对象在一开始就创建,用来记录整个过程。Tour of the API

走 Hosted Checkout 时,你通常不自己调用 paymentIntents.create,但 Dashboard 和 Webhook 里仍会看到 PaymentIntent。自定义表单时,后端创建它并返回 client_secret,前端用 Stripe.js 完成确认。

stateDiagram-v2
    [*] --> requires_payment_method: 创建 PaymentIntent
    requires_payment_method --> requires_confirmation: 附上 PaymentMethod
    requires_confirmation --> requires_action: 需要 3D Secure 等
    requires_confirmation --> processing: 确认支付
    requires_action --> processing: 客户完成认证
    processing --> succeeded: 扣款成功
    processing --> requires_capture: 仅预授权
    processing --> requires_payment_method: 失败,可换卡重试
    requires_capture --> succeeded: 捕获资金
    requires_payment_method --> canceled: 取消
    requires_confirmation --> canceled: 取消
    requires_action --> canceled: 取消
status含义应用侧
requires_payment_method还没有可用的支付方式,或上次被拒继续收集 PaymentMethod,不要履约
requires_confirmation已有支付方式,等待确认大多数集成会在确认时一并提交支付方式,看不到这一步
requires_action需要 3D Secure 或其他客户动作交给 Stripe.js 处理,不要当成失败
processing正在处理,常见于银行扣款可以提示“处理中”,仍不能发货
requires_capture已授权、尚未请款只有预授权流程会用到
succeeded资金已进入账户可以履约;退款走 Refunds API
canceled已作废不能再对它收款

paymentIntents.create 的最小输入是 amount 和 currency;输出里最重要的是 id 和 client_secret。client_secret 只证明“前端正在完成这一笔 PaymentIntent”,不能代替 Secret key,也不能当作“已经付款”的证据。前端回调同样不可靠:窗口可能被关掉,恶意客户端也可以伪造成功提示。最终状态仍以 Webhook 为准。Payment Intents、How Payment Intents work

SetupIntent 看起来很像 PaymentIntent,但只保存支付方式、不扣款。Checkout 的 mode: 'setup' 就是这条路径。

七、订阅:Product、Price、Invoice 如何转起来

订阅不是“每月自动再跑一次 Checkout”。创建 Subscription 之后,Stripe 按周期生成 Invoice,再为这张 Invoice 创建 PaymentIntent,用客户保存的 PaymentMethod 收款。About the Billing APIs

flowchart TB
    A["创建 Subscription"] --> B{"需要立即付款吗"}
    B -->|"要,且尚未支付"| C["status: incomplete"]
    C --> D["生成 Invoice 与 PaymentIntent"]
    D --> E{"23 小时内付款成功?"}
    E -->|是| F["status: active"]
    E -->|否| G["status: incomplete_expired"]
    B -->|"有 Trial"| H["status: trialing"]
    H --> F
    F --> I["周期结束生成下一张 Invoice"]
    I --> J{"续费成功?"}
    J -->|是| F
    J -->|否| K["status: past_due"]
    K --> L["按 Dashboard 的失败付款设置进入 unpaid 或 canceled"]

第一次接入订阅,仍然优先用 Checkout:mode: 'subscription',line_items 里放 recurring Price。Stripe 会在结账成功后创建 Customer、PaymentMethod 和 Subscription。之后让客户管理账单,用 Billing Portal,而不是自己做一套“改卡 / 取消”后台。

billingPortal.sessions.create 输入 customer 和 return_url,输出带 url 的短期会话。必须由后端根据已登录用户找到对应 Customer,不能让浏览器传来任意 cus_...。

订阅状态值得单独记住,因为“有一条订阅”不等于“现在能用付费功能”:How subscriptions work

status含义授权时注意
trialing试用中可以按产品策略授权
active当前周期有效不保证历史 Invoice 全部已付
incomplete首笔款未在窗口内完成不要当成正式会员
incomplete_expired首笔款超时需要重新创建 Subscription
past_due最新已定稿 Invoice 付款失败宽限期由你的失败付款设置决定
unpaid已停止自动扣款,订阅仍在官方建议此时收回产品访问
canceled终态不能再改,只能新建
paused试用结束且缺少默认支付方式时可能进入要恢复需补上支付方式

7.1 AI credits 订阅 + add-on:先确定卖的是什么

本文以下实操采用一个明确的示例:每月订阅发放 1,000 AI credits;用户可另外购买 500 credits 的一次性 add-on。数字仅用于说明,不是推荐定价。适用范围是商家直接使用自己的 Stripe 账户、Customer 与 API v1 Checkout / Billing 对象,不涉及 Connect 或 Accounts v2 迁移。

英文术语中文名称简要解释
AI creditsAI 使用额度应用定义的消耗单位,可以按模型、token 或任务换算
add-on附加购买项可能是一次性额度包,也可能是每期收费的附加订阅项,必须明确是哪种
Credit Grant额度发放记录Stripe Billing 中的正式对象;本文自建数据库的发放批次不冒充该对象
Proration按比例计费调整周期中途改套餐产生的金额调整,不自动等于 AI credits 调整
Snapshot event对象快照事件携带事件发生时对象数据的 Event;处理时仍可能需要回查
Thin event精简事件需要按对应 API 获取关联对象的另一种事件形态,不能直接套用快照解析逻辑
Test Clock测试时钟推进受支持的 Billing 测试对象时间,用于观察续费等变化
收费项Stripe 配置何时发 AI credits应用中的记录
每月基础额度recurring Price,Checkout mode=subscription对符合规则的 invoice.paid 发放绑定 Invoice line、Subscription item 与对应周期
一次性 add-onone-time Price,独立 Checkout mode=paymentSession 确认已付款后发放绑定 Checkout Session 与 line item
每期续费的 add-onSubscription 中额外的 recurring Price / Subscription item对应已付 Invoice line 按周期发放与基础套餐分开记账

一次性 add-on 不要误配成 recurring Price,否则用户会周期性被收费。若把 one-time Price 混入 mode=subscription 的初次 Checkout,它只进入初始 Invoice;这时应由 Invoice 履约分支识别该行,不能再走独立 payment Session 的发放规则。本文先采用“两种 Checkout 分开”的方案,使发放来源更容易追踪。Create a Checkout Session

AI credits are an application-level unit. Stripe 收取货币,应用记录可消费的 AI credits。Stripe 的 Billing credits / Credit Grant 用于符合条件的计量账单抵扣;当前文档要求相关 Subscription item 使用 metered Price 并通过 Meters 报告用量,不适用于所有固定价格订阅或一次性账单。选择“固定订阅费 + 预付额度包 + 应用实时扣减”时,可以先自建额度账本,无须因为名字里有 credits 就接入 Credit Grant。若未来改成 Stripe 按实际用量出账,再评估 Meters 与 Billing credits 的完整方案。Billing credits

开始编码前,把以下规则写进产品规格;这是应用决策,不是 Stripe 的默认行为:

  • 订阅额度是否到期、是否结转;本文示例按已付账单对应周期到期,不结转。
  • add-on 是否到期、是否必须保持订阅才能使用;本文示例独立保留、无自动续费,并允许取消订阅后继续使用。实际规则需在销售时说明。
  • 消耗顺序;本文建议先扣最早过期的批次,避免新周期发放覆盖已购 add-on。
  • 试用、100% 折扣、升级补差、退款后的额度策略;这些都不能从“金额是否大于零”简单推导。
  • 年付套餐是一次发全年额度,还是每月释放;若每月释放,应用需要以已付年费权益为依据安排定时任务,不能等待每月 invoice.paid,因为年付本来就不按月出账。

7.2 Customer、Price 和订单如何关联

在 Sandbox 的 Product catalog 中先建基础套餐的 monthly recurring Price,再建额度包的 one-time Price。保存两者的 Price ID,在服务端维护映射,例如 price → { kind, credits, policyVersion }。真实价格、币种、数量范围和折扣策略都由服务端确认,前端只提交业务商品编号。新旧 Price 对应的历史规则应保留,否则重放旧付款会按新额度发货。

Customer 应绑定计费主体:个人产品绑定用户,团队产品绑定组织。后端从已认证会话确定主体,创建或复用 Customer,并在本地持久化一对一映射;不能根据浏览器提交的 Customer ID 或付款邮箱认领账户。创建 Checkout 前,先保存本地 pending 订单及规则快照,保证 Webhook 即使早于创建请求的后续写库,也能找到归属。

以下是服务端参数示意,不是完整 HTTP 路由。customerId、orderId、priceId 必须来自前述服务端记录;stripe 是已初始化的 Node SDK 客户端:

js
const session = await stripe.checkout.sessions.create(
  {
    mode: 'payment', // 基础订阅单独创建,改为 subscription 并选 recurring Price
    customer: customerId,
    line_items: [{ price: priceId, quantity: 1 }],
    client_reference_id: orderId,
    metadata: { order_id: orderId, kind: 'credit_pack' },
    payment_intent_data: { metadata: { order_id: orderId } },
    success_url: 'https://example.com/billing/success?session_id={CHECKOUT_SESSION_ID}',
    cancel_url: 'https://example.com/billing',
  },
  { idempotencyKey: `checkout:${orderId}` },
);

创建订阅时用 subscription_data.metadata 写入内部计费主体 ID,并移除 payment_intent_data。不要把“改 mode”理解成其他参数全能照搬。Session 的 metadata 不会任意复制到所有关联对象;向 Subscription 和 PaymentIntent 传 metadata 各有明确参数。Webhook 应优先使用本地 Customer / 订单映射,metadata 用于关联与排查,不作为客户端自行声明的权限依据。Metadata

相同订单的网络重试复用 Idempotency key;换商品、数量或重新购买,应创建新订单和新 key。Stripe 请求幂等只处理 API 重试,不能代替应用账本的唯一约束。Idempotent requests

7.3 应该勾选哪些 Webhook 事件

以下清单分为付款闭环、运营处理和条件事件。订阅某个事件只是让 Stripe 投递通知,仍需后端实现对应分支。invoice.paid 是订阅额度的统一入口;Checkout 事件仅给 mode=payment 的一次性额度包发放。

层级Event收到后具体做什么是否发额度
核心checkout.session.completed回查 Session;subscription 模式只补关联,payment 模式核对状态、Customer、订单与商品仅合格的一次性额度包
核心,兼容延迟支付checkout.session.async_payment_succeeded调用与 completed 相同的额度包履约函数同一 Session 只能发一次
核心,兼容延迟支付checkout.session.async_payment_failed回查并更新待付款订单,提示重新付款;不覆盖已经确认的成功状态否
核心invoice.paid找到 Subscription 与全部 Invoice lines,按商品和周期规则发放仅符合资格的行
核心invoice.payment_failed记录失败账单,引导改卡 / 付款;同步催收状态否,不补发新周期额度
核心customer.subscription.created建立订阅镜像;可能为 incomplete 或 trialing否
核心customer.subscription.updated同步套餐、状态、取消计划;回查以避免旧快照覆盖新状态否
核心customer.subscription.deleted标记该订阅终止,按有效期停止后续发放否,不清空所有 add-on
运营建议invoice.payment_action_required引导客户完成认证,可使用对应 Hosted Invoice Page;避免只报“付款失败”否
运营建议invoice.finalization_failed告警并检查 last_finalization_error;未定稿不能收款否
运营建议refund.created、refund.updated、refund.failed同步 Refund 最新状态,关联原订单,执行冻结 / 冲正 / 恢复策略不增加购买额度
运营建议charge.dispute.created、charge.dispute.closed建立争议记录、风险冻结;结案后依据最新结果处理不盲目重发
按需checkout.session.expired回收未完成订单、更新状态否
按需customer.subscription.trial_will_end试用到期提示;若给试用额度,另设受限发放规则否
按需customer.subscription.paused、customer.subscription.resumed使用暂停 / 恢复订阅功能时同步状态不凭事件直接加额度
按需entitlements.active_entitlement_summary.updated接入 Stripe Entitlements 时同步功能权限不替代数值额度账本

付款闭环先选表中的 8 个核心事件;上线时建议把认证、定稿失败、退款和争议分支一起完成。只开放即时卡支付时,两个 async 事件可能暂时不触发;保留处理可避免以后增加支付方式时遗漏。事件名称与语义见 Event types、Subscription webhooks、Refund events。

这里刻意不把以下事件作为新的发放入口:

  • payment_intent.succeeded、charge.succeeded:同一交易还会产生 Checkout / Invoice 事件。若每个都加额度,会重复履约。
  • invoice.payment_succeeded:表示一次 Invoice 付款尝试成功;invoice.paid 还覆盖将 Invoice 标记为已付的情形。统一采用后者,再明确哪些已付方式有资格发放,不要两个都各发一份。
  • invoice.created、invoice.finalized:都不表示已付。没有编辑账单等明确需求时,不为“看起来完整”全部订阅;尤其 invoice.created 未得到成功响应,可能延迟自动定稿,最长达 72 小时,具体有例外条件。
  • charge.refunded:包含部分退款;若同时使用 Refund 事件,应进入同一套按 Refund ID 去重的处理,不能各扣一次额度。

零金额、余额抵扣或人工标记已付的 Invoice,并不等于一次新的银行卡扣款。免费试用产生的零元账单不要误发付费套餐额度;合法 100% 优惠券是否给完整额度则是另一条商品规则。对人工标记已付设置明确的后台操作权限和履约策略。Invoice events、Subscription webhooks

八、Webhook:权限真正改变的地方

Stripe 用 Event 描述账户里发生的事情,再用 Webhook 把 Event POST 到你的 HTTPS 端点。异步结果都走这条路:银行确认到账、客户拒付、订阅续费成功或失败。前端回跳只是给用户看的入口。Receive Stripe events

sequenceDiagram
    participant Stripe as Stripe
    participant Endpoint as 应用 Webhook 端点
    participant Queue as 可靠存储 / 队列
    participant Worker as 履约处理

    Stripe->>Endpoint: POST Event 原始 JSON
    Endpoint->>Endpoint: constructEvent 验签
    alt 验签失败
        Endpoint-->>Stripe: 400
    else 验签成功
        Endpoint->>Queue: 按 event.id 幂等写入
        Endpoint-->>Stripe: 2xx
        Queue->>Worker: 异步履约
        Worker->>Worker: 回查最新对象并更新权限
    end

接入时固定这几件事:

  1. 验签必须用原始 body。 Node 里是 stripe.webhooks.constructEvent(rawBody, stripeSignature, endpointSecret)。先 JSON.parse 再 JSON.stringify,签名会坏掉。
  2. Signing secret 不是 API key。 每个端点有自己的 whsec_...。Stripe CLI 本地转发时打印的 secret,和 Dashboard 里正式端点的 secret 不是同一个。
  3. 先可靠接收,再返回 2xx。 验签后先将事件写入持久化 inbox 或可靠队列;成功后立即响应,再异步做复杂履约。持久化失败返回 5xx,不能提前确认。超时、3xx 重定向和 5xx 都算投递失败。
  4. 按 Event ID 和业务主键做幂等。 同一 Event 会被重投;履约函数也可能被成功页和 Webhook 各调用一次。
  5. 不要假设到达顺序。 创建订阅可能连续产生 customer.subscription.created、invoice.created、invoice.paid。需要最新状态时,用对象 ID 再 retrieve 一次。

Live mode 下,Stripe 会用指数退避重试最多三天;Sandbox 则在数小时内重试三次。Dashboard 可在 15 天内手动 Resend。最多注册 16 个 Webhook 端点。本地没有公网 HTTPS 时,用:

bash
stripe listen --forward-to localhost:4242/webhook

Receive Stripe events

通用的 URL、HTTPS、签名和重试约定,见 WebHook URL 概念。

8.1 Dashboard:从空白配置到第一条成功投递

以下按 2026-09-17 的官方 Workbench 流程说明。界面文案可能调整,应以账户实际 UI 为准;本文没有代替读者在真实账户中创建端点。

  1. 先切到目标 Sandbox。 先不要在 Live mode 测试。确认当前 Product、Price、Customer 和后端 API key 都属于同一个环境。
  2. 准备接收路由。 例如 POST https://api.example.com/stripe/webhook。这是服务端接收地址,不是 Checkout success_url,也不是前端页面。外网端点必须 HTTPS;只对该路由豁免交互式登录 / CSRF 限制,改由 Stripe 签名认证,不能取消整个应用的保护。避免路径末尾重定向。
  3. 进入 Workbench → Webhooks → Create an event destination。 官方直达入口是 Dashboard Webhooks。
  4. 事件来源选 Your account。 本文是商家收自己的款;Connected accounts 的平台事件属于 Connect 场景。
  5. 确定事件形态和 API version。 本文处理 event.data.object 的 Snapshot events;若界面提供 Snapshot / Thin 选择,不要拿 Thin payload 直接运行本例。将选定的事件 API version 写入工程配置记录,并核对 SDK 类型及服务端 API 返回结构。升级 SDK 不会自动改写既有事件。
  6. 勾选上节事件。 用完整名称搜索并选取,先完成核心 8 项,再加入实际实现的运营和条件分支。无需 Select all events。
  7. Continue → Webhook endpoint → Continue。 填写 Endpoint URL 与可辨识的说明,例如 credits-sandbox,完成创建。
  8. 复制该端点的 signing secret。 在设置中 Reveal secret,将 whsec_... 写入后端 STRIPE_WEBHOOK_SECRET 并重启 / 重新部署服务端。不要写进前端或提交到 Git。
  9. 在该 Sandbox 真正完成一笔测试购买。 在 Event deliveries 查看事件类型、Event ID、请求内容、响应码和重试记录;再检查应用 inbox、订单和额度账本。200 只能证明接收成功,不能证明额度已发放。
  10. Live mode 单独配置。 测试通过后,单独创建 Live Price、配置 Live API key、Webhook endpoint 与 secret,核对环境映射。Sandbox endpoint 不会自动变成生产端点。

API key 用于应用调用 Stripe;Webhook signing secret 用于验证 Stripe 的来信。两者不能互换。同一代码可以服务多个环境,但对象 ID、数据库记录和密钥必须隔离。Receive Stripe events

8.2 本地开发:CLI 的 secret 与 Dashboard 的 secret 分开

已安装并登录 Stripe CLI、应用监听本地 4242 端口后,运行:

bash
stripe listen \
  --events checkout.session.completed,checkout.session.async_payment_succeeded,checkout.session.async_payment_failed,invoice.paid,invoice.payment_failed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted \
  --forward-to localhost:4242/stripe/webhook

把这次 stripe listen 输出的 whsec_... 配给本地接收进程。它对应 CLI 转发,不是 Dashboard 创建的公网端点。修改事件清单后,CLI 的过滤清单也要同步;Dashboard 的选择不会自动替换这条命令的参数。Local testing

另开一个终端可做基础连通性检查:

bash
stripe trigger checkout.session.completed

这条命令会创建测试 fixture 及关联事件,不能证明你真实的 Customer、Price、订单 metadata 与额度规则已经贯通。正式验收应从应用的购买按钮进入自己的 Checkout,使用 Sandbox 测试支付方式;CLI trigger 主要用于检查路由、验签和事件分发。

现象优先检查
没有投递记录环境 / 账户是否一致、事件是否勾选、实际操作是否产生该事件
CLI 收到而 Dashboard 公网端点没收到两条投递路径独立;检查公网端点 URL 与事件范围
400 验签失败raw body 是否被解析或修改、使用了哪一个 whsec_、服务器时间是否正确
301 / 302 / 307 / 308URL 是否跳转、登录中间件或尾斜杠规则是否拦截
401 / 403登录墙、CSRF、WAF 是否拦截专用路由
500 / 超时inbox 持久化或处理依赖是否失败;修复后重投
200 但没有额度inbox 是否仍 pending / failed、Customer 是否映射、Price 是否识别、是否进入错误模式分支
额度翻倍Checkout 与 Invoice 双重发放、缺少业务唯一键、并发事务不完整

8.3 从 Event 到额度:两层去重与可靠接收

A successful delivery is not a completed fulfillment. 接收端应验签,按 (环境, 账户, event.id) 将事件持久化为 pending,然后返回 2xx。数据库 / 队列写入失败返回 5xx,让 Stripe 重试。不能先返回 200,再仅靠当前进程的一段异步代码尝试写库;进程退出会永久丢失已经确认的任务。

后台处理器从可靠 inbox 取任务,回查 Stripe 对象并校验业务条件,然后在一个数据库事务中完成:插入唯一发放记录、增加对应额度批次、标记任务完成。若业务数据和队列不共用事务,则以数据库 outbox / inbox 和可重试消费者衔接,并以发放唯一键保证重试不重复。不能先把 Event 标为 processed,再执行可能失败的加额度。

去重层建议的唯一键解决的问题
Event 接收环境 + Stripe 账户 + Event ID同一个 Event 重投
一次性额度包环境 + 账户 + Session ID + line item ID + purchasecompleted 与 async success 进入同一处理;并发重试
订阅发放环境 + 账户 + Invoice ID + Invoice line ID + cycle_grant同一账单多次通知、多条收费项、任务重跑
退款冲正环境 + 账户 + Refund ID + 原发放 ID + reversal一次退款多个状态事件、部分退款逐次处理
AI 消耗计费主体 + AI 请求 ID + 操作类型请求重试、超时回调、重复结算

这些是本文建议的应用数据约束,不是 Stripe 自动替你创建的字段。数据库应有真正的唯一约束或等价的串行化事务;“先查询有没有,再插入”如果没有并发保护,仍然会重复发放。

一份可排错的数据模型至少保留:Customer 映射、订单及规则快照、Subscription 镜像、Webhook inbox、额度发放批次、消费流水、退款 / 争议关联。额度批次记录 granted、remaining、expires_at、来源 ID、币种金额关联和规则版本。展示余额来自仍有效且未冻结的批次,不能在续费时执行 balance = 1000,把用户另买的 500 credits 覆盖掉。

对于 AI 请求,开始前原子预留额度,完成后按实际用量结算,失败则按定义释放;长任务保留预留记录与过期回收策略。只在请求结束时扣余额,会让多个并发请求同时通过余额检查。这属于应用账本设计,不由 Stripe Webhook 解决。

8.4 一次性 add-on 的处理顺序

checkout.session.completed 与 checkout.session.async_payment_succeeded 调用同一个处理函数:

  1. 按 Session ID retrieve 最新 Session,并读取全部 line items;若 has_more,继续分页,不只处理 expand 返回的第一页。
  2. 校验 mode=payment、环境、Customer 与本地订单一致;验证 Price、quantity、币种与允许的折扣 / 税额规则,额度数量来自服务器订单快照,不能从客户端 metadata 的数字直接取。
  3. 正常付费额度包要求 payment_status=paid。unpaid 保持待付款;no_payment_required 只在明确允许免费额度包时单独处理,不偷偷当成付费成功。
  4. 以 Session / line item 的唯一键在事务中发放;已发放则安全返回已有结果。
  5. 保存 PaymentIntent 等关联,方便 Refund / Dispute 回溯;成功页读取自己的订单和余额接口,查询前验证订单属于当前用户。

延迟支付时 completed 可以先到,成功事件后到;失败事件不能简单把所有同 Customer 的额度清零。Stripe 也明确要求履约函数能处理同一个 Session 的重复、并发调用。Fulfill orders

8.5 订阅每期发放:不能收到任意 Invoice 就加 1,000

对 invoice.paid:

  1. retrieve 最新 Invoice,确认 status=paid;从 Invoice 的订阅关联找到 Subscription,而非只依赖尚未到达的 Checkout 事件。找不到本地映射时暂存并重试 / 告警,不丢弃账单。
  2. 区分订阅账单与普通一次性 Invoice,分页读取全部 lines。只对服务器已登记的额度产品发放;未知 Price 进入人工检查,不能套默认套餐。
  3. 检查 billing_reason。subscription_create、subscription_cycle 是常见初次 / 续期入口;subscription_update 等进入单独调整规则,不自动发一整期。还要检查行项目是否为 Proration,仅按 billing_reason 判断不够。
  4. 根据该已付 Invoice line 对应的 Price、quantity 和 period计算额度与有效期。回查 Subscription 用于当前状态;不能用“现在的套餐”给几个月前迟到的账单算额度。
  5. 校验试用、折扣、人工标记已付及旧周期补缴规则。已过期周期的迟到付款不应误开一个从今天开始的新周期;记录历史履约与到期结果,必要时进入支持流程。
  6. 对每个合格行按业务唯一键事务发放。Subscription 镜像的更新也要防止并发回查结果以相反顺序落库,适合按 Subscription ID 串行处理或使用版本检查。

字段路径要匹配版本。 当前 Invoice / Invoice line reference 使用 parent、pricing 等结构:订阅关联可位于 invoice.parent.subscription_details.subscription,价格可位于 line.pricing.price_details.price,订阅行的 Proration 信息可位于 line.parent.subscription_item_details.proration,周期使用 line.period.start/end。旧教程的 invoice.subscription、line.price、line.proration 不应不经检查直接复制。按已固定 API version 解析,检查 parent.type 等判别字段,并用真实 Sandbox payload 作回归样本。Invoice object、Invoice line item

变化示例处理策略不能做什么
到期续费成功旧批次按原有效期到期;新账单产生新批次覆盖 add-on 余额
到期取消记录取消计划,已付周期继续有效;实际结束停止未来发放一看到 cancel_at_period_end 就没收当前额度
续费失败暂不发新周期;宽限期是否允许使用现有批次由产品规则确定把一次失败当成所有订阅终止
周期中升级最简单是下周期生效;若立即生效,成功补差后仅发规则确定的差额subscription.updated 一到就发高档整期额度
周期中降级示例建议下周期生效,本周期按已售条款处理根据金额退款比例猜应扣多少 AI credits
暂停收款区分 pause_collection 与 Subscription paused 状态,另定权限政策假设所有暂停方式触发相同事件

Stripe Proration 调整的是钱。例如原套餐 1,000 credits 升到 3,000 credits,剩半个周期,若产品规则是按剩余周期补额度,可设计为补 1,000 credits;这只是业务计算示例,不是 Stripe 自动算出的额度。税、折扣、已消费数量与金额舍入都可能使“补差金额 ÷ 每 credit 单价”失真。立即升级还要考虑付款失败时是否应用变更,可评估 pending updates;上线前需验证 Billing Portal 中允许的套餐变更同样遵守额度策略。Prorations、Pending updates、Pause payment collection

8.6 退款与 Dispute:沿原发放批次处理

refund.created 只表示创建 Refund,未必最终成功。回查最新 Refund,按 pending、succeeded、failed 等状态处理:可先冻结原购买剩余额度,成功后记录一次冲正,失败后按规则释放冻结;直接成功的 created 也要能处理,不能只等 updated。官方推荐至少监听 refund.created。Refunds

假设购买 500 credits、已经消费 200,只剩 300。全额退款时应明确剩余 300 的撤销,以及已消费 200 的处理政策,例如人工审核、记欠额或按条款拒绝自助全额退款。不能机械执行 balance -= 500,把用户另一笔正常购买的额度一起扣掉。部分退款按已确认的退款订单与规则分配到原批次;包含税、折扣、多商品的付款不能仅凭总退款百分比猜每种商品该扣多少。

Refund 经 PaymentIntent / Charge 关联原支付,再通过本地订单 / Invoice 支付关联定位发放记录。订阅退款不等于取消订阅,取消也不等于退款。若同时收到争议与退款,不应对同一批次做两次撤销;统一累计已冲正额度并保留原因。charge.dispute.closed 也不等于赢得争议,需看最新 Dispute 结果后按既定规则解冻或维持撤销。Dispute events

退款的 Dashboard / API 操作、事件处理、状态表和独立验收见第十二节。

8.7 验收:看账本结果,不只看 Dashboard 的绿色成功

场景操作方式应得到的结果
首次订阅从应用创建 Checkout 并完成 Sandbox 付款只有合格 Invoice line 发一次基础额度;Checkout 不重复发
次期续费用 Test Clock 支持的 Customer / Subscription 推进周期新 Invoice 产生新批次;旧批次按原有效期处理
同一事件重投Workbench 手动 ResendEvent 不重复入账;余额不变
两个事件对应同一额度包completed 与 async success,或处理器并发运行Session / line item 只产生一次发放
延迟支付使用官方对应异步测试流程completed 未付款时不发;最终成功才发,失败不发
验签失败错 secret 或修改请求 body返回 400;无入账记录
接收存储故障在测试环境让 inbox 写入失败返回 5xx;修复后 Stripe 重试成功
后台处理崩溃在事务提交前后分别中断并重跑未提交可重试;已提交不重复;失败任务可发现
Webhook 乱序用真实已记录事件按不同顺序重放不依赖 created / completed 先到;旧状态不覆盖新状态
周期中升级测试付款成功、认证与失败仅按升级规则补额度,失败不提前发付费差额
取消在 Portal 到期取消及恢复取消当前已付周期保留;独立 add-on 按条款保留
试用 / 零元账单试用和 100% 折扣分别测试区分试用额度与合法折扣权益,无额外重复赠送
全额 / 部分退款已消费与未消费的订单分别退款对原批次只冲正一次;失败退款能解除对应冻结
年付每月释放已付年费后推进应用定时任务并重跑同一释放月份只发一次,不依赖月度 Invoice
两个 AI 请求并发余额只够其中一个请求时同时请求预留事务阻止超额消费,重试不重复扣减

Test Clock 仅适用于 Stripe 支持的 Billing 测试流程;不要假设所有 Checkout / 支付方式或退款争议都能靠推进时钟模拟。按官方文档创建兼容对象,等待相关处理完成后再看事件与账本。Test your Billing integration

运维上还需定期对账:将已付且符合资格的 Invoice / Session 与应用发放记录逐项比较,补处理漏单,并告警长期 pending / failed 的 inbox。Stripe 的投递重试窗口有限,返回过 200 但后台失败的任务尤其需要自己的重试与告警。重放时使用原商品规则版本,不能按当前价格表重算历史购买。

九、API key、Sandbox 和 Live mode

Stripe 把测试和正式收款分成两套互不相通的对象空间。Sandbox 的 Customer、Price、Webhook secret 都不能拿到 Live mode 用。API key 前缀可以直接辨认环境。API keys

类型前缀能否出现在前端用途
Publishable keypk_test_ / pk_live_可以Stripe.js、Elements、移动 SDK
Restricted API keyrk_test_ / rk_live_不可以按权限裁剪的后端密钥,官方推荐新集成优先使用
Secret keysk_test_ / sk_live_不可以未限制权限的后端密钥
Webhook signing secretwhsec_不可以只用于验签,不是 API key

创建账户后可以立刻在 Sandbox 里模拟交易,不移动真实资金。要收真钱,必须完成企业验证并激活 Live mode。激活后不能改商家所在国家;要换支持国家,只能开新账户。Set up your account、Activate your account

Secret key 只放在服务端密钥库或环境变量里。官方明确:Stripe 员工不会向你索要密钥。Webhook 端点必须 HTTPS,并校验 Stripe-Signature。Integration security guide

十、关键方法:记住输入和输出就够

下面按 Node SDK 的常见资源写法列出。其他语言是同一组 API,只是客户端名称不同。不同语言 SDK 的客户端结构可能不同,不能把其他语言的 StripeClient 写法直接套到 Node SDK。

方法输入输出
customers.createemail、metadataCustomer,含 id(cus_...)
checkout.sessions.createmode、line_items、success_url、可选 customerCheckout Session,含 id 与 url
checkout.sessions.retrieveSession ID,expand: ['line_items']最新 Checkout Session
paymentIntents.createamount、currency、可选 customerPaymentIntent,含 client_secret
billingPortal.sessions.createcustomer、return_urlBilling Portal Session,含 url
webhooks.constructEvent原始 body、Stripe-Signature、whsec_已验签 Event
refunds.createpayment_intent 或 charge,可选 amountRefund
prices.createcurrency、unit_amount、可选 recurring、productPrice

这些调用都发生在后端。前端只应拿到 Publishable key、Checkout url,或某一笔交易的 client_secret。

十一、第一次接入的最小路径

不要同时做自定义卡表单、Connect 分账和 Usage Based Billing。先用 Sandbox 跑通下面这条链:

  1. 创建 Stripe 账户,只使用 pk_test_ / sk_test_ 或对应 Restricted API key。
  2. 在 Dashboard 建一个 Product 和一个 Price。一次性收款用普通 Price;订阅用 recurring Price。
  3. 后端根据已登录用户创建或复用 Customer,再 checkout.sessions.create。
  4. 浏览器打开 url,用测试卡付款。
  5. 用 Stripe CLI 把 Webhook 转到本地,按第七节选择事件、按第八节验签并可靠接收;订阅与额度包使用分开的履约入口。
  6. 成功页只显示“正在确认”,再向自己的后端查询权限。
  7. 用 Billing Portal 测试改卡和取消。
  8. 故意制造失败、重复投递和延迟支付,确认不会多发货。

上线前再换一套 Live mode 的 API key、Price ID、Webhook 端点和 signing secret。费率与支持国家以当前 Pricing 和 Global availability 为准,不要沿用过期教程里的数字。

商家主体所在地和客户所在地是两件事。能否开商家账户,看官方支持国家名单;支持国家的商家可以向许多国家的客户收款。账户激活后不能改国家。中国大陆主体能否直接开户,以 stripe.com/global 当前名单为准,不要根据第三方“注册攻略”推断。如果需要 Merchant of Record 而不是自己成为开票主体,回到 Polar 那条路径。

十二、退款怎么做:主动发起、Webhook 同步与额度冲正

Refund initiation and refund reconciliation are separate operations. 发起退款是商家通过 Dashboard 或后端 API 请求 Stripe 退钱;Webhook 是 Stripe 把退款创建、状态变化与失败通知给应用。只订阅事件不会替你发起退款,只调用退款 API 也不会替你撤销 AI credits。Refund and cancel payments

对于“订阅每期发基础额度 + 一次性 add-on”,上线前应有完整的退款同步与人工处理路径。早期可以不做用户自助退款按钮,由管理员在 Dashboard 操作;但应用仍应同步退款状态、冻结或撤销对应批次的 credits,并记录审计流水。退款资格、已消费额度如何处理、金额如何计算属于产品与销售条款;下面是工程实现建议,不是 Stripe 统一替商家决定的政策。

12.1 两个退款入口,使用同一条同步路径

入口谁操作应用必须做什么
Stripe Dashboard有权限的商家运营人员依靠 Webhook 发现 Refund,关联原订单并更新额度
应用后台的退款 API经授权的管理员,或通过退款资格审核的用户请求先建立内部退款申请,再调用 Stripe;API 返回与 Webhook 共用幂等同步逻辑
客户向银行提出 Dispute客户 / 发卡行进入争议处理分支,不把它伪装成用户点击退款按钮

Dashboard 操作步骤:选择正确 Sandbox / Live mode → Payments → 找到原付款 → 菜单中的 Refund payment → 确认全额或输入部分金额 → 选择原因 → 确认 Refund。核对原 Customer、金额、币种和订单,记录生成的 re_...,再到 Workbench 查看退款事件是否送达、应用账本是否处理。Issue refunds

退款针对原付款的 PaymentIntent / Charge,不能把 Checkout Session ID cs_...、Subscription ID sub_... 或 Invoice ID in_... 直接当成 Refund API 的 payment_intent。一次性额度包从订单保存的 Session / PaymentIntent 关联回溯;订阅退款先确定具体账单及其实际付款,不能随意退 Customer 最近的一笔钱。若账单由余额等方式结清,或有多笔付款,应按真实付款记录处理,不能假定每张已付 Invoice 都有一个可直接退款的 PaymentIntent。

API 发起示例,以下是已初始化 Node SDK 客户端中的参数片段,不是包含鉴权与数据库的完整路由:

js
// 以下值来自后端已审核、持久化的退款申请,不直接信任客户端输入。
const refund = await stripe.refunds.create(
  {
    payment_intent: originalPaymentIntentId,
    amount: approvedRefundAmount, // 正整数,单位为原币种最小单位
    reason: 'requested_by_customer',
    metadata: { refund_request_id: refundRequestId },
  },
  { idempotencyKey: `refund:${refundRequestId}` },
);
// 保存 refund.id 与 refund.status,交给统一同步逻辑;不要直接写死“退款成功”。

例如 USD 的 amount: 600 表示退 6 美元。省略 amount 表示退剩余可退金额;部分退款必须显式计算金额。同一付款可以多次部分退款,但累计不能超过可退金额。原支付尚未 capture 的情形可能需要取消 PaymentIntent,而不是创建 Refund。Create a refund、Refunds

API 发起前的顺序建议是:

  1. 验证操作权限和订单归属;从服务端确认实际支付、已退款金额、未完成退款申请及消费情况。
  2. 在本地事务中创建退款申请和对应额度冻结,防止审核后、退钱前这批额度继续被消费;同一订单的并发申请需要互斥或额度预留。
  3. 调用 Stripe,保存返回的 Refund ID;相同申请的网络重试使用同一个 Idempotency key。
  4. 若网络超时,标为“结果待确认”,回查或用同一个 key 重试,不立刻解冻,也不换 key 重新退款。Stripe 调用与本地数据库不是一个分布式事务。
  5. API 返回对象与后续 Webhook 共用按 Refund ID 去重的状态处理。Webhook 可能先到,内部申请和原付款映射应在调用前已存在。

Dashboard 发起会绕过这条本地预冻结步骤。因此,即使保留手工入口,也需要 Webhook 尽快同步,并为已经消费、无法映射或金额不符合自动策略的退款建立人工检查队列。

12.2 Webhook 要订阅什么,收到后做什么

在第八节的同一个 event destination 增加以下事件即可;通常不需要另建“退款专用 URL”。选中事件后,服务端也要实现对应处理,并更新 CLI 过滤参数。

Event事件对象处理动作
refund.createdRefund建立 / 更新退款记录,回查当前状态;若已经 succeeded,立即进入幂等冲正,不能只等 updated
refund.updatedRefund回查最新状态,推进冻结、冲正或恢复;更新也可能只是 metadata / 追踪信息变化,不能每次都扣 credits
refund.failedRefund记录失败原因、告警,核对原冲正与冻结后补偿;必要时安排人工退款支持
charge.refunded(可选)Charge用作付款维度的核对信号,查询对应 Refund;包含部分退款,不能一收到就清空整单额度

本方案以前三个 Refund 事件为主要入口。charge.refund.updated 是旧事件,当前文档标为 deprecated,新的实现优先用 refund.updated。若现有 endpoint 固定在旧 API version,应先核对该版本的事件兼容性,再安排升级,不能假设修改 SDK 就改变投递结构。Refund events

本地单独调试退款时可用下面的过滤命令;实际同时测试购买与退款时,把这些名字追加到第八节原有清单,而不是替换掉付款事件:

bash
stripe listen \
  --events refund.created,refund.updated,refund.failed \
  --forward-to localhost:4242/stripe/webhook

验签、持久化 inbox、响应 2xx 与失败重试仍使用 8.3 的流程。处理退款事件时不要再次调用 refunds.create;该分支的职责是回查和同步已经存在的 Refund,否则可能形成重复退款。

建议把三类事件都交给同一个处理函数。下面是处理流程伪代码,applyRefundStateTransactionally 是应用自行实现的事务接口:

js
switch (event.type) {
  case 'refund.created':
  case 'refund.updated':
  case 'refund.failed': {
    const refund = await stripe.refunds.retrieve(event.data.object.id);
    await applyRefundStateTransactionally(refund);
    break;
  }
}

该事务接口应校验环境与账户,沿 payment_intent / charge 找原付款、订单和额度批次,并按下表处理。找不到关联时保留待处理任务并告警,不能把 Event 丢掉或用邮箱猜归属。

最新 refund.status金钱状态示例额度策略
pending退款处理中,可能与余额不足等因素有关保留对应额度冻结,不标记最终成功
requires_action退款还需要额外动作检查 next_action 并进入支持流程,保留相关冻结
succeededStripe 记录退款成功;客户银行入账显示可能有延迟对已批准的原批次记录一次冲正;不要宣称余额已经出现在客户银行 App
failed退款失败记录 failure_reason;若此前已冲正,用补偿流水纠正;仅在没有其他冻结理由时恢复可用额度
canceled退款已取消同步取消结果,按原申请撤销冻结 / 补偿;不能理解成取消 Subscription

状态来自 Refund object。不要仅把第一次看到的 succeeded 当作永远不可能变化:退款后续也可能失败,应保留补偿路径。所有变化按 Refund ID 串行或加锁处理,防止并发回查结果反向覆盖。过期批次恢复后仍遵守原有效期,不能借失败退款生成新额度。

12.3 AI credits 如何撤销:先定位批次,再决定数量

以一个无税、无折扣、单商品的示例说明:用户花 10 美元买了 500 credits,已使用 200,剩余 300。如果产品已约定仅退未用部分,可批准退 6 美元并撤销剩余 300;如果人工决定全额退 10 美元,则还要明确已消费 200 的成本由商家承担、记欠额还是采用其他政策。不能一边全额退款,一边默认剩余 300 继续可用。

实际存在税、折扣、多商品、赠送额度时,不能照抄这个比例公式。退款申请应保存批准金额、涉及的原发放 ID、计划撤销额度及政策版本。默认只动原购买批次,避免从其他正常订单扣走额度。

必须覆盖两个先后顺序:

  • 先发额度、后退款:按原发放 ID 冲正;用 (Refund ID, 原发放 ID, 操作类型) 去重,重复事件不重复扣减。
  • 先收到退款、后处理购买事件:即使还没有发放记录,也持久化退款 / 冻结事实;之后处理 Checkout 或 Invoice 时,在同一受保护的事务中检查原付款的退款状态,只发净合格额度,或创建后立即冲正为零。不能因为“还没找到额度批次”就忽略退款,随后又全额发放。

部分退款可能有多个 re_...。分别记录每次金额和额度分配,同时核对累计冲正,避免退款、Dispute、人工调整对同一份额度重复撤销。不要删除历史消费记录;账本保留购买、消耗、冻结、冲正与补偿,使余额能被重算和解释。

12.4 订阅退款与取消必须分开决策

用户意图金钱操作Subscription 操作AI credits 操作
只退一次性 add-on对该订单付款创建 Refund通常不改基础订阅只处理该额度包批次
退本期订阅款,但保留以后续费对明确选定的本期付款退款保留订阅,并向用户说明后续续费按本期退款政策处理基础额度
到期不再续费通常不需要 Refund,除非另有退款决定设置周期末取消当前已付周期按销售条款继续有效
立即终止并退款单独计算、执行退款同时执行明确的取消策略处理本期权益;独立 add-on 按其条款保留或另退

某些 Dashboard 的取消流程会让你同时选择退款选项,但这不等于“取消与退款是一个对象”:后端仍分别同步 Subscription 与 Refund。不要在 customer.subscription.deleted 里无条件创建全额 Refund,也不要在 refund.created 里无条件取消该 Customer 的所有订阅。Cancel subscriptions

已出具 Invoice 的账单金额调整还可能涉及 Credit Note(贷项通知单)。它用于调整已定稿账单,与现金退款不是同义词。对已付 Invoice,Credit Note 可以关联退款、计入客户余额或记录站外处理;若已通过 Refund API 退过钱,再处理 Credit Note 时应关联既有退款,避免再次退钱。具体路径按账单调整方式选择。Credit notes

12.5 Dispute、Payout 与 Connect 各自做到哪一步

Dispute 不是商家发起的普通退款。建议至少处理 charge.dispute.created 和 charge.dispute.closed:前者记录争议、定位原付款并按策略冻结相关额度,同时交给有权限的运营人员处理证据与期限;后者回查 Dispute 结果,决定维持撤销还是恢复。结案可能赢也可能输;赢得争议也不是再赠送一次原始额度。若还要自动跟踪资金扣回与返还,可进一步处理 charge.dispute.funds_withdrawn / charge.dispute.funds_reinstated;它们用于资金对账,不能直接触发新额度发放。Event types

Payout 是 Stripe Balance 到商家银行的打款,与用户付款履约分开。用户支付确认后,不应等 payout.paid 才发 credits;payout.failed 也不表示该用户付款失败。普通 SaaS 初期可以在 Dashboard 管理打款,按运营需求增加到账 / 失败提醒。

Connect 用于多商户平台、Connected Account 和分账。自己销售 AI credits 不因此需要 Connect。若确实涉及平台收费,则退款还要考虑由哪个账户承担、是否退 application fee、是否 reverse transfer,这属于另一条资金方案,不能照搬本文普通商家示例。Refunds through a Connect platform

12.6 退款链路的最低验收清单

  1. 从应用真实创建的 Sandbox 订单完成支付,再在 Dashboard 全额退款:三个 Refund 事件入口能被接收,原批次按状态变化,无需点击应用里的退款按钮。
  2. 分两次部分退款,分别保留 Refund ID;重投每个事件后,累计退款金额和额度冲正都不翻倍。
  3. 用同一内部退款申请重试 API,并模拟请求超时:不创建第二笔退款,不提前解除不确定状态的冻结。
  4. 已消费、未消费、已过期、含税 / 折扣的订单分别检查金额与额度政策;任何自动策略无法解释的情况进入人工审核。
  5. 退款事件先于额度发放事件到达时,最后净可用额度仍正确。
  6. 使用 Stripe 对具体支付方式支持的测试流程验证失败 / 后续失败;无法直接触发的分支以状态样本测试并记录验证范围,检查冲正补偿不会恢复已过期或仍有争议冻结的额度。
  7. 订阅取消、订阅退款、add-on 退款分别测试,确认不会误取消其他订阅或清空其他额度包。

以上是应实施的验收方案;本文未对读者的 Stripe 账户执行退款。Dashboard 看到退款成功、Webhook 返回 200、应用账本完成冲正是三个不同的检查点。

十三、放到 React、TanStack、Cloudflare、Convex 中

职责可以按现有优先技术栈划分,但不需要为了使用这些技术而改写 Stripe 的对象模型。

  • React + HeroUI 展示套餐、结账按钮和“正在确认”状态。
  • TanStack Start 的服务端路由创建 Checkout Session 和 Billing Portal Session,Price ID 来自服务端白名单。
  • TanStack Query 读取应用自己的权限接口,不把 Stripe 对象直接暴露给浏览器。
  • Webhook 在 Cloudflare Workers 或其它后端验签后,写入可靠队列再更新权限;不要在 Worker 请求时限内做发邮件、发货等长任务。
  • 若后端已经是 Convex,可以评估 @convex-dev/stripe:它提供 getOrCreateCustomer、createCheckoutSession,并用 registerRoutes 接入验签后的 Webhook。Component 会把订阅状态存进自己的表,但客户映射、Price 白名单和“用哪些 Event 授权”仍要按本文核对。

受保护的操作必须在服务端检查权限。隐藏按钮不能代替授权。

对应的站内资料:

十四、资料来源

基础内容整理日期:2026-09-08;PayPal 关系与接入说明核验日期:2026-09-16;AI credits、Webhook 配置与履约资料核验日期:2026-09-17。产品能力、国家名单和 API 字段以当前官方文档为准。

本文共 17234 字,创建于 Sep 8, 2026

相关标签:系统设计, HTTP, TanStack, ByAI, AI

博客助手

正在打开博客助手…