Chapter 7 · Reading the ledger
The Platform Refund Ledger · 7 min read
This chapter answers one question: how do you reconstruct what actually happened to your money from balance transactions alone, and reconcile a full day by hand?
Every object in chapters 1 through 6 has an opinion about what happened. Balance transactions are not opinions. They are the double-entry record of every unit that entered or left each Stripe balance, and Stripe recommends them as "our recommended starting point for reporting on your account's balance activity" (docs.stripe.com/reports/balance-transaction-types).
Anatomy of a balance transaction
| Field | Meaning |
|---|---|
type | What kind of movement (catalog below) |
amount | The movement itself, in the smallest currency unit |
fee / fee_details | Stripe's own fees attached to this line, itemized |
net | amount − fee |
status | pending or available |
available_on | When status flips to available — settlement timing lives here |
source | The ID of the object that caused the line: a charge, refund, transfer, dispute… |
reporting_category | Stripe's grouping for accounting use, which its docs prefer over raw type for classification |
The source field is the join key for every audit in chapter 8: a balance transaction never floats free; it points at its cause.
The types that matter on a Connect platform
Definitions follow Stripe's balance-transaction-types page; the last two columns show direction from the platform account's point of view:
| Type | Stripe's definition, condensed | Platform sees |
|---|---|---|
charge | A successful card charge | + gross |
refund | A card refund initiated | − refunded |
application_fee | Earnings from Connect platform fees; written on direct charges, where the fee credits your balance as its own line | + fee |
application_fee_refund | Platform fees returned to connected accounts | − returned |
transfer | Funds sent to connected accounts | − sent |
transfer_refund | Transfers reversed; "reversals add to your platform's balance and subtract from the connected account's balance" | + reversed |
adjustment | Outside-the-flow debits/credits; disputes debit here with the dispute as source | ± dispute amounts and fees |
stripe_fee | Fees for Stripe software and services, including Connect | − fees |
payout, payout_cancel, payout_failure | Money to/from your bank | − / + / + |
reserve_transaction | Funds reserved because a connected account went negative | held against you |
connect_collection_transfer | After 180 days negative, funds moved to zero the account out | − you pay |
The shape detail that breaks spreadsheets
Chapter 1 promised this consequence. On a destination charge priced with application_fee_amount, the fee is withheld from the transfer rather than sent out and returned: the full charge lands on your balance, the charge less the fee moves on to the seller, and Stripe's processing fee comes out of your share (docs.stripe.com/connect/destination-charges). The platform ledger therefore reads: charge +120.00, stripe_fee −3.78, transfer −108.00. Nowhere in that day is a +12.00 credit. The platform's cut is the gap between the charge and the transfer, and a reader who goes hunting for a fee line concludes the fee was never collected.
The ApplicationFee object does exist, carrying amount 1200, and it is what refund_application_fee acts on later — it simply is not a movement. Priced instead with transfer_data[amount] of $108.00, the same sale writes the same two money lines, but no fee object exists at all and your cut is visible only in the export's Destination Platform Fee column. Identical cash, different evidence: one shape leaves something you can refund and audit, the other leaves you reconstructing it from order records. Filter your day by source objects, not by pattern assumptions.
A full day, reconciled by hand
One fabricated day on one fabricated destination-charge platform, priced with application_fee_amount, so each transfer is its sale less that sale's fee. Opening available balance: $400.00. Five sales ($120/$80/$50/$200/$90, each with a 10% fee), Stripe processing at 2.9% + 30¢, one full default refund, one 25% partial refund handled with both flags, one nightly payout of $150.00.
| Time | Type | Source | Amount | Running |
|---|---|---|---|---|
| — | — | opening available | — | 400.00 |
| 09:14 | charge | ch_A ($120) | +120.00 | 520.00 |
| 09:14 | stripe_fee | ch_A | −3.78 | 516.22 |
| 09:14 | transfer | tr_A (fee_A $12.00 withheld) | −108.00 | 408.22 |
| 09:41 | charge | ch_B ($80) | +80.00 | 488.22 |
| 09:41 | stripe_fee | ch_B | −2.62 | 485.60 |
| 09:41 | transfer | tr_B (fee_B $8.00 withheld) | −72.00 | 413.60 |
| 11:02 | charge | ch_C ($50) | +50.00 | 463.60 |
| 11:02 | stripe_fee | ch_C | −1.75 | 461.85 |
| 11:02 | transfer | tr_C (fee_C $5.00 withheld) | −45.00 | 416.85 |
| 13:20 | charge | ch_D ($200) | +200.00 | 616.85 |
| 13:20 | stripe_fee | ch_D | −6.10 | 610.75 |
| 13:20 | transfer | tr_D (fee_D $20.00 withheld) | −180.00 | 430.75 |
| 14:37 | charge | ch_E ($90) | +90.00 | 520.75 |
| 14:37 | stripe_fee | ch_E | −2.91 | 517.84 |
| 14:37 | transfer | tr_E (fee_E $9.00 withheld) | −81.00 | 436.84 |
| 15:03 | refund | re_1 (all of ch_A) | −120.00 | 316.84 |
| 16:10 | refund | re_2 (25% of ch_D) | −50.00 | 266.84 |
| 16:10 | transfer_refund | reversal of tr_D | +45.00 | 311.84 |
| 16:10 | application_fee_refund | 25% of fee_D | −5.00 | 306.84 |
| 18:00 | payout | po_Nightly | −150.00 | 156.84 |
Verify the three checks a hand reconciliation always runs:
- Category totals. Charges +540.00 − transfers 486.00 = +54.00, the day's withheld fees, appearing as a gap rather than as credits. Stripe fees −17.16 (3.78+2.62+1.75+6.10+2.91), refunds −170.00, reversal +45.00, fee return −5.00, payout −150.00. Sum: 54.00 − 17.16 − 170.00 + 45.00 − 5.00 − 150.00 = −243.16.
- Balance identity. Opening 400.00 + net −243.16 = 156.84 closing — matches the running column exactly.
- Payout support. At 18:00 the available balance was 306.84; the $150.00 payout is covered and leaves 156.84. A payout larger than the
available(notpending) balance would be the first anomaly of the day.
Notice what the ledger shows that no dashboard summary did: the 15:03 refund cost $120.00 with no compensating line at all, while the 16:10 trio nets to −$10.00. Not −$5.00, either — the reversal reaches only the $180.00 that was actually transferred, so 25% of it is $45.00 against a $50.00 refund, and the $5.00 fee return lands on top of that. Same afternoon, opposite policies, visible only as line items.
[DIAGRAM: From event to ledger]
Left column: business events (Charge, Transfer, ApplicationFee,
Refund, Reversal, FeeReturn, Payout).
Middle: the objects they create (ch_, tr_, fee_, re_, trr_/via refund,
fr_, po_).
Right: the balance transactions each writes, with type labels.
Arrows one-to-many except reversals, annotated "one refund can write
zero, one, or three ledger lines depending on flags".
Caption: reconciliation is walking this diagram right to left.
What to check on your own platform
- Export one recent day of balance transactions and reproduce this chapter's three checks by hand, once, before automating anything.
- Group the day by
typefirst, then bysource— confirm every destinationtransferequals its charge less the fee, and that anApplicationFeeobject exists wherever you expect to be able to refund one. - Search the export for
transfer_refundlines and match every one to the refund that motivated it; unmatched lines are findings. - Search for
adjustmentlines with dispute sources and confirm each appears in your disputes register. - Compare
status: pendingversusavailabletotals at close of day; the gap is next week's payout, and miscounting it is the classic month-end surprise (chapter 9).
Sources: docs.stripe.com/reports/balance-transaction-types · docs.stripe.com/api/balance_transactions/object · docs.stripe.com/connect/destination-charges