代码 Stripe Connect 退款成功、平台余额却变负:reverse_transfer 和 refund_application_fee 决定钱从谁那扣

2026-10-25 09:01:33

平台退款成功、钱没回平台:Stripe Connect 三种扣费模型的退款与拒付边界

退款接口返回 200,用户端看到钱到账,平台的 Stripe 余额却变成负数;或者子账户的钱早已转走,退款时平台垫了全款,事后想追回,却因为子账户余额不足直接报错。两种现场指向同一类问题:退款本身没失败,失败的是「这笔钱该从谁的余额扣、transfer 要不要反向」。

官方文档:Connect charges、destination charges、separate charges and transfers。

三种扣费模型,判据是「谁承担退款与拒付」

direct charges:charge 建在 connected account 上,钱进子账户余额;退款和拒付扣子账户;平台通过 application_fee_amount 抽成。子账户需要 card_payments capability。

destination charges:charge 建在平台账户,付款成功后全额立刻进子账户 pending balance,application_fee_amount 再转回平台。平台承担 Stripe 手续费、退款、拒付。marketplace 常用。

separate charges and transfers:平台先 charge,再单独 transfers 到多个子账户,charge 与 transfer 解耦。适合一单分给多方、或下单时还不知道收款方。平台余额承担手续费、退款、拒付;transfer 默认从平台 available balance 出,超过可用余额直接失败且不会自动重试。

背概念不如记判据:退款和拒付最终从谁的余额扣。direct 扣子账户,平台不垫钱;destination 和 separate 扣平台,平台垫钱。这个差别决定了后面所有参数和排查方向。

destination charge:reverse_transfer 与 refund_application_fee

destination charge 的默认行为是:子账户保留已经转走的钱,退款全额从平台余额扣,平台余额可能因此变负。

想追回,创建 refund 时带 reverse_transfer=true:

curl https://api.stripe.com/v1/refunds \
  -u "$STRIPE_SECRET_KEY:" \
  -d payment_intent=pi_xxx \
  -d amount=5000 \
  -d reverse_transfer=true

全额退款会反向整笔 transfer;部分退款按退款金额比例反向 transfer。

application fee 默认留在平台。要退给子账户,加 refund_application_fee=true,且退 application fee 时必须同时 reverse transfer。部分退款同样按比例。

const refund = await stripe.refunds.create({
  payment_intent: 'pi_xxx',
  amount: 5000,
  reverse_transfer: true,
  refund_application_fee: true,
});

参数单位是最小货币单位(分)。退款应对 PaymentIntent.latest_charge 对应的支付发起。

这里有两种「余额不足」,行为完全不同:

  • 平台余额不足:refund 状态为 pending,余额够了 Stripe 自动处理并转 successful。
  • refund 同时带 transfer 反向,而子账户余额不足:请求直接报错,不会创建 pending refund。

两者都叫余额不足,一个挂起、一个失败。排查时先看请求有没有带反向 transfer。(文档口径,未在 live 模式验证)

refund_application_fee 不能单独开:退 application fee 必须同时 reverse transfer。

separate charges and transfers:退款和 transfer 没有联动

退款对 transfer 没有任何影响,必须自己调 reversals:

curl https://api.stripe.com/v1/transfers/tr_xxx/reversals \
  -u "$STRIPE_SECRET_KEY:" \
  -d amount=2000

amount 可传部分反向。只有子账户 available balance 足够(或开了 connected reserves)才能反向;需要换汇且换完余额为 0 会报错。失败或取消的退款,金额回到平台余额,需要再自己 transfer 出去。关闭子账户的退款能力不影响 transfer reversal。

工程负担在这里:每一笔退款都要自己判断对应的 transfer 是否反向、反向多少。charge 和 transfer 之间没有 Stripe 自动维护的退款关联。

拒付:挑战中别反向,输了再反向

  • direct charge:扣子账户余额。
  • destination / separate charges:扣平台余额,平台再 reverse transfer 追回。

时间点很重要:挑战中先别反向;赢了不反向;输了再反向。赢了之后反向还给子账户,可能因平台余额不足或跨境限制失败。所以反向要放在拒付结果之后,而不是收到 dispute.created 就做。

source_transaction 与异步支付的例外

transfer 带上源 charge:

curl https://api.stripe.com/v1/transfers \
  -u "$STRIPE_SECRET_KEY:" \
  -d amount=9000 \
  -d currency=usd \
  -d destination=acct_xxx \
  -d source_transaction=ch_xxx

约束如下:即使平台可用余额不够也返回成功,但资金要等该 charge 结算才可用;金额不得超过源 charge;同一 source 可多次 transfer,总和不超过 charge;自动生成 transfer_group;币种必须一致。

异步支付方式(ACH/SEPA Debit)不要用 source_transaction:支付后续失败不会自动反向,平台余额会被扣,需要手动 reverse transfer。destination charge 遇到异步支付失败,Stripe 会自动 reverse transfer。

异步支付期间子账户丢能力或被关号,transfer 会被 skip,charge 里 transfer_data 为 null,要监听 charge.updated 才能发现,事后再补 transfer。

transfer_group 只是业务串联标签,不影响资金;真正控制「等钱到再转」的是 source_transaction。

工程落地:本地字段与对账口径

退款从 PaymentIntent.latest_charge 发起,本地至少存:payment_intent id、latest_charge id、transfer id(destination 和 separate 都要)、transfer_group、退款单 id 与金额、是否 reverse_transfer、是否 refund_application_fee,以及 dispute id 和状态。

对账按 charge 维度串起来:平台余额变动 = 退款 + 拒付 + 手续费 − 反向 transfer − 退回的 application fee;子账户余额变动 = transfer − reversal − application fee + refund_application_fee。每笔退款要能回答两个问题:钱从哪个余额扣的、对应的 transfer 有没有反向。

其他会影响账务的事实:on_behalf_of=子账户 时以子账户为 settlement merchant,在子账户国家/币种结算、用该国费率、账单描述用子账户的;跨 region 的 destination charge 通常必须带。application_fee_amount 封顶为整笔金额,币种与交易一致,跨境的 application fee 可能按子账户结算币种入账。

选型:按退款拒付的承担方选,不是按抽成方式选

  • direct:平台不碰资金,退款拒付扣子账户,平台不需要垫钱。适合只提供工具、不做交易担保的平台,抽成用 application_fee_amount。
  • destination:平台统一收款,承担手续费、退款、拒付。适合 marketplace。默认不开 reverse_transfer 就是平台垫全款;子账户余额不足会直接报错,需要预留余额或改走 separate。
  • separate charges and transfers:一单分给多方、下单时收款方未知。退款与 transfer 解耦,reversals 要自己做,工程量和垫资都在平台侧。

最容易选错的是 destination:抽成方便,但退款和拒付的账都落在平台余额上。如果子账户会频繁提现、余额长期为 0,reverse_transfer 会在退款时直接报错。此时要么让子账户留余额,要么接受平台垫资,要么换 separate 自己控制转账节奏。

复制全文 生成海报 Stripe 支付对接 分账 退款 出海 对账

推荐文章

程序员茄子在线接单