The Platform Refund Ledger

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

FieldMeaning
typeWhat kind of movement (catalog below)
amountThe movement itself, in the smallest currency unit
fee / fee_detailsStripe's own fees attached to this line, itemized
netamount − fee
statuspending or available
available_onWhen status flips to available — settlement timing lives here
sourceThe ID of the object that caused the line: a charge, refund, transfer, dispute…
reporting_categoryStripe'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:

TypeStripe's definition, condensedPlatform sees
chargeA successful card charge+ gross
refundA card refund initiated− refunded
application_feeEarnings from Connect platform fees; written on direct charges, where the fee credits your balance as its own line+ fee
application_fee_refundPlatform fees returned to connected accounts− returned
transferFunds sent to connected accounts− sent
transfer_refundTransfers reversed; "reversals add to your platform's balance and subtract from the connected account's balance"+ reversed
adjustmentOutside-the-flow debits/credits; disputes debit here with the dispute as source± dispute amounts and fees
stripe_feeFees for Stripe software and services, including Connect− fees
payout, payout_cancel, payout_failureMoney to/from your bank− / + / +
reserve_transactionFunds reserved because a connected account went negativeheld against you
connect_collection_transferAfter 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.

TimeTypeSourceAmountRunning
opening available400.00
09:14chargech_A ($120)+120.00520.00
09:14stripe_feech_A−3.78516.22
09:14transfertr_A (fee_A $12.00 withheld)−108.00408.22
09:41chargech_B ($80)+80.00488.22
09:41stripe_feech_B−2.62485.60
09:41transfertr_B (fee_B $8.00 withheld)−72.00413.60
11:02chargech_C ($50)+50.00463.60
11:02stripe_feech_C−1.75461.85
11:02transfertr_C (fee_C $5.00 withheld)−45.00416.85
13:20chargech_D ($200)+200.00616.85
13:20stripe_feech_D−6.10610.75
13:20transfertr_D (fee_D $20.00 withheld)−180.00430.75
14:37chargech_E ($90)+90.00520.75
14:37stripe_feech_E−2.91517.84
14:37transfertr_E (fee_E $9.00 withheld)−81.00436.84
15:03refundre_1 (all of ch_A)−120.00316.84
16:10refundre_2 (25% of ch_D)−50.00266.84
16:10transfer_refundreversal of tr_D+45.00311.84
16:10application_fee_refund25% of fee_D−5.00306.84
18:00payoutpo_Nightly−150.00156.84

Verify the three checks a hand reconciliation always runs:

  1. 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.
  2. Balance identity. Opening 400.00 + net −243.16 = 156.84 closing — matches the running column exactly.
  3. 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 (not pending) 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

  1. Export one recent day of balance transactions and reproduce this chapter's three checks by hand, once, before automating anything.
  2. Group the day by type first, then by source — confirm every destination transfer equals its charge less the fee, and that an ApplicationFee object exists wherever you expect to be able to refund one.
  3. Search the export for transfer_refund lines and match every one to the refund that motivated it; unmatched lines are findings.
  4. Search for adjustment lines with dispute sources and confirm each appears in your disputes register.
  5. Compare status: pending versus available totals 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