The Platform Refund Ledger

Chapter 1 · The money graph of Stripe Connect

The Platform Refund Ledger · 7 min read

This chapter answers one question: what objects does Stripe actually create when money moves on a Connect platform, and how do they connect?

Everything else in this book — refund paths, reversals, fees, disputes, reconciliation — is operations performed on this object model. Platforms get surprised by refunds because they picture one graph and Stripe drew another. So we start by drawing all of them.

The objects

Every backward movement of money is a record in one of these objects. The names are Stripe's; the field lists are the ones this book will use repeatedly.

ObjectWhat it recordsFields used in this book
AccountAny party: your platform, or a connected accountid (acct_…), capabilities, negative-balance responsibility settings
ChargeOne payment attempt that succeededamount, application_fee_amount, transfer_data, on_behalf_of, refunded, amount_refunded
RefundMoney returned to the buyeramount, charge, status, reason, balance_transaction, transfer_reversal
TransferFunds moved from the platform balance to a connected account's balanceamount, amount_reversed, destination, source_transaction, reversals
TransferReversalPart or all of a transfer pulled backamount, transfer, balance_transaction, refund_application_fee
ApplicationFeeThe platform's cut of a charge, as its own objectamount, amount_refunded, refunded, charge
FeeRefundPart or all of an application fee given back (object: fee_refund)amount, fee, balance_transaction
DisputeA cardholder-initiated challenge to a chargeamount, status, reason, evidence, balance_transactions
BalanceTransactionOne line of money in or out of an account balance — the ground truthtype, amount, net, fee, status, available_on, source
PayoutMoney leaving a Stripe balance for a bank accountamount, status, arrival_date

The single most important habit in Connect accounting: the business event ("the customer got a refund") and the ledger evidence (a set of balance transactions) are different things, and only the second one is true. Chapters 7 and 8 are about reading that evidence.

Three charge patterns, three graphs

Stripe supports three ways to structure a sale (docs.stripe.com/connect/charges), and they produce genuinely different money graphs.

Direct charges. The charge is created on the connected account, using your secret key with the connected account's ID in the Stripe-Account header. Funds settle into the connected account's balance; your platform receives only the application fee. The transaction objects exist on the connected account, not on your platform (docs.stripe.com/connect/direct-charges).

Destination charges. The charge is created on your platform with transfer_data[destination] set. As a side effect of the charge being captured, funds are transferred to the connected account's pending balance (docs.stripe.com/connect/destination-charges).

Separate charges and transfers. The charge is created on your platform with no transfer_data. You create Transfer objects later, explicitly — possibly several, possibly days later, linked loosely by source_transaction or transfer_group (docs.stripe.com/connect/separate-charges-and-transfers).

[DIAGRAM: The three money graphs]
Three panels, left to right, each showing the same $120 sale.

Panel 1 — Direct charge:
  Buyer --> Charge ch_1 --> CONNECTED ACCOUNT balance (+$120)
  Connected account balance -- application_fee_amount $12 --> PLATFORM balance (+$12)
  Note under panel: charge object lives on the connected account;
  platform never holds the $120.

Panel 2 — Destination charge:
  Buyer --> Charge ch_2 --> PLATFORM balance (+$120)
  PLATFORM balance -- Transfer tr_1 ($108 = $120 - $12) --> CONNECTED ACCOUNT
  PLATFORM balance retains $12, recorded as ApplicationFee fee_1
  Note under panel: charge object lives on the platform; the transfer
  is created automatically because transfer_data[destination] was set,
  and the fee is withheld from it rather than sent and returned.

Panel 3 — Separate charges & transfers:
  Buyer --> Charge ch_3 --> PLATFORM balance (+$120)
  PLATFORM balance -- Transfer tr_2 ($108, chosen by you) --> CONNECTED ACCOUNT
  Note under panel: nothing connects ch_3 and tr_2 automatically unless
  source_transaction was passed; no ApplicationFee object exists.

One sale, traced three ways

Take the same sale — a buyer pays $120.00, the platform's cut is $12.00 (10%), the Stripe processing fee is $3.78 (2.9% + 30¢, the standard US card pricing, used throughout this book) — through each pattern. Every line below is a balance transaction or its immediate cause.

LegDirectDestinationSeparate
Charge lands+$120.00 → seller balance+$120.00 → platform balance+$120.00 → platform balance
Platform cut+$12.00 → platform (ApplicationFee created)withheld from the transfer (ApplicationFee created for $12.00)implicit: you simply transferred less
Transfer outnone−$108.00 → seller balance ($120.00 − $12.00)−$108.00 → seller balance (your choice of amount)
Stripe processing fee−$3.78 from seller−$3.78 from platform share−$3.78 from platform share
Seller ends with$104.22$108.00$108.00
Platform ends with+$12.00+$8.22+$8.22

Check the arithmetic once, slowly: direct — 120 − 12 − 3.78 = 104.22 to the seller, 12 to the platform. Destination — the platform holds 120, sends 108, keeps the 12 it withheld, and pays 3.78: 120 − 108 − 3.78 = 8.22. Separate — 120 − 108 − 3.78 = 8.22. Same sale, same buyer, three different sets of objects carrying the money — and note that the destination and separate columns move identical amounts. What separates them is not the cash but the evidence: destination leaves a fee object and a charge-to-transfer link, separate leaves neither.

Notice two structural facts that drive everything in chapters 2 through 6:

  1. Only the direct and destination patterns create an ApplicationFee object. In the separate pattern the platform's cut is just residue — it exists because you chose to transfer less than the charge. A parameter aimed at a fee object has nothing to act on there.
  2. On destination charges the application fee is withheld from the transfer, not sent out and returned. Price with application_fee_amount and the transfer to the connected account is the charge less the fee — $120.00 − $12.00 = $108.00 — while the ApplicationFee object records the $12.00 the platform kept (docs.stripe.com/connect/destination-charges). Price instead with transfer_data[amount] and you name the transferred figure yourself, and no fee object exists at all — the Dashboard's Balance history export shows the platform's cut in a Destination Platform Fee column instead. Both shapes net the platform the same cash; only one of them leaves a fee object you can refund. Read transfer.amount; never reconstruct it from the charge. Half of the reconciliation errors this book describes come from assuming one shape while running the other — and because every reversal in chapters 3 through 8 is computed against transfer.amount, an assumed $120.00 there generates expectations no transfer could ever satisfy.

Which pattern am I on?

From the data, per charge: if charge.transfer is populated, it is a destination charge. If the charge lives on a connected account (you had to pass Stripe-Account to read it), it is a direct charge. If it sits on your platform with no linked transfer, it is separate charges and transfers, and any transfers are independent objects you joined yourself.

A platform can run more than one pattern at once — a marketplace that onboarded a subscription product or a new seller category often does. That is also how a correct refund path becomes incorrect: the code branches on an assumption instead of on the charge.

What to check on your own platform

  1. Classify your last ten charges by pattern using the field test above, not by what the code was supposed to do.
  2. For each destination charge, read transfer.amount and confirm it equals the charge less the application fee; a different figure means you are pricing with transfer_data[amount] and have no fee object to refund.
  3. Confirm whether any of your flows create ApplicationFee objects at all — check the Collected fees view in the Dashboard or list them via the API.
  4. If you run more than one pattern, find every place your code issues refunds and verify each one branches on the charge's actual pattern.

Sources: docs.stripe.com/connect/charges · docs.stripe.com/connect/direct-charges · docs.stripe.com/connect/destination-charges · docs.stripe.com/connect/separate-charges-and-transfers