The Platform Refund Ledger

Chapter 4 · Application fees and their refunds

The Platform Refund Ledger · 5 min read

This chapter answers one question: what is the complete lifecycle of an application fee — creation, collection, refund, and partial refund — and what does "the platform keeps the fee" mean in ledger terms?

The application fee is the platform's revenue line, and it has an independent life of its own: it can outlive the sale it came from, be returned piecemeal or whole, and sit quietly on your balance after everything around it has been undone.

Creation and collection

A fee exists wherever you price with application_fee_amount on a PaymentIntent or Checkout Session (direct and destination patterns; chapter 1 explains why the separate pattern has none). On success Stripe creates an ApplicationFee object carrying:

  • amount — the total fee, e.g. 1200 ($12.00);
  • amount_refunded — the running total returned so far;
  • refunded — flips to true once the entire fee has been refunded;
  • charge — the parent charge, the join key every audit in chapter 8 starts from.

Collection mechanics differ subtly by pattern, and both are documented. On direct charges the fee is deducted inside the connected account's settlement — "the resulting charge's BalanceTransaction includes a detailed fee breakdown of both the Stripe and application fees" — and a credit lands on your platform balance as a balance transaction of type application_fee. On destination charges the fee never leaves in the first place: the charge settles to your balance in full, only the charge-less-fee amount transfers on to the seller, and the ApplicationFee object records the difference you retained (docs.stripe.com/connect/direct-charges, docs.stripe.com/connect/destination-charges).

One timing detail matters for anyone building listeners: "Application fees for direct charges are created asynchronously by default," and the notification arrives as an application_fee.created event rather than inside the charge response (docs.stripe.com/connect/direct-charges). A reconciliation job that assumes the fee object exists at charge-creation time will race it.

Refunding the fee alongside the charge

Passing refund_application_fee: true on a refund delegates to Stripe's proportional rule: a full charge refund returns the full fee; a partial refund returns "an amount proportional to the amount of the charge refunded." The API reference adds the permission boundary: "An application fee can be refunded only by the application that created the charge" — the platform, always (docs.stripe.com/api/refunds/create). For destination charges remember the pairing rule from chapter 2: refunding the fee requires reversing the transfer too.

Refunding the fee standalone

Fees left attached to refunded revenue have their own endpoint: POST /v1/application_fees/{id}/refunds. It takes an optional amount — omit it and the entire unrefunded remainder goes back — and returns a FeeRefund object ("object": "fee_refund") with its own balance_transaction (docs.stripe.com/api/fee_refunds/create). Partial fee refunds are ordinary: you may return part repeatedly until nothing remains.

The money direction is worth stating precisely, because it surprises people: the funds go back to the Stripe account from which the fee was originally collected — the connected account. A fee refund debits your platform balance and credits the seller's. This is the correct instrument for correcting a kept fee after the fact, for promotional waivers, and for any policy where the platform compensates the seller without touching the buyer's refund.

"Keeping the fee," in ledger terms

Work the same episode three ways. Setup: a $250.00 destination charge, application_fee_amount $25.00 — so the fee is withheld and transfer.amount is $225.00 (chapter 1) — later refunded $100.00 (40%).

LegFee returned proportionallyFee keptNothing else done
Charge+$250.00+$250.00+$250.00
Transfer to seller (fee withheld)−$225.00−$225.00−$225.00
Buyer refund−$100.00−$100.00−$100.00
Transfer reversal (round(0.40 × 225))+$90.00+$90.00none
Fee returned (round(0.40 × 25))−$10.00nonenone
Platform lifetime position+$5.00+$15.00−$75.00

Read each column down and add: 250 − 225 − 100 + 90 − 10 = 5; drop the last line and it is 15; drop the reversal too and it is −75. Every column is the same refund; the platform's outcome ranges from keeping a fifth of its fee to losing $75.00 of cash. No error message distinguishes them.

The middle column is the quietly correct one, and it is worth seeing why. Because the fee was withheld from the transfer at the outset, a proportional reversal claws back precisely the seller's share of the refunded slice and leaves yours alone: the buyer's remaining $150.00 splits into $135.00 for the seller and $15.00 for you — the promised 10%, preserved without anyone having to decide anything.

The left column is the one finance teams should argue about. Returning $10.00 of fee on top of that reversal compensates the seller for a deduction the reversal has already unwound, so your take on the remaining $150.00 falls to 5 ÷ 150 ≈ 3.3% while the seller ends at $145.00 of a $150.00 sale. That may be exactly what your terms provide — a goodwill posture on platform-fault refunds, say — but it should be a written decision, not a flag someone set by habit. The right column needs no argument at all: it is the default path, and it is simply a loss.

Fee refunds appear in your ledger as balance transactions of type application_fee_refund, defined by Stripe as "platform fees that you have returned to your connected accounts" (docs.stripe.com/reports/balance-transaction-types). Chapter 7 places them in the day's ledger; chapter 8 counts them against expected proportions.

What to check on your own platform

  1. List your collected fees for the last 90 days and count how many sit on charges with amount_refunded > 0 while amount_refunded = 0 on the fee itself.
  2. For those, compute the expected proportional share — round(refunded ÷ charged × fee) — and compare against what was actually returned.
  3. Confirm your refund code paths set refund_application_fee deliberately per reason, not uniformly.
  4. Check whether any support process refunds fees manually and whether those events are recorded anywhere besides Stripe's audit trail.
  5. Verify your seller terms describe the fee behavior your code actually implements (chapter 10 makes this systematic).

Sources: docs.stripe.com/api/application_fees/object · docs.stripe.com/api/fee_refunds/create · docs.stripe.com/api/refunds/create · docs.stripe.com/reports/balance-transaction-types