Chapter 10 · Designing a refund policy on purpose
The Platform Refund Ledger · 5 min read
This chapter answers one question: how do you turn "who absorbs each kind of refund" from an API default you inherited into a decision you made, encoded in flags, before a dispute forces the question?
Most platforms did not choose their refund policy. Chapter 6's matrix is its real author: whatever path issues refunds without setting the parameters chooses the expensive cells by omission. This chapter converts the matrix into four dials, encodes them in code, and writes down what everyone else has to be told.
Dial 1 — the transfer
Who funds the seller's share of a refund?
| Setting | Encoding | Consequence |
|---|---|---|
| Reverse always | reverse_transfer: true on every refund | Seller-funded; requires terms and a communication path for post-payout negatives |
| Reverse proportionally by reason | flag derived from reason (below) | The deliberate default this chapter argues for |
| Absorb below a floor | flag true only above threshold X | Buys silence on small amounts; chapter 8 still counts every one |
Worked comparison on the book's $120 destination sale: reverse-always returns the $108.00 that was transferred and leaves both you and the seller at exactly zero; absorb-always costs you $108.00 net of the fee you withheld. Across a quarter these settings differ by refunds × 108 — compute your last quarter's count before choosing anything.
Dial 2 — the fee
What happens to your cut? The answer differs by pattern, which is why this dial is set wrong so often. On direct charges, keeping the fee compensates genuinely sunk processing cost but raises the effective take rate on partially refunded sales (chapter 6's 13.3% case). On destination charges the fee was withheld from the transfer to begin with, so a proportional reversal already preserves your promised ratio exactly — returning the fee there is not a correction but a decision to compensate the seller on top (chapter 4). Doing that only on platform-fault refunds splits the difference along blame lines your support team already assigns. All three are expressible — refund_application_fee on the refund call, or a standalone application_fees.refund with an explicit amount when the split is partial by design.
Dial 3 — disputes
Two sub-decisions: whether to claw back at all (the chapter 5 table's middle column needs contractual footing), and whether to reverse early on charge.dispute.created or wait for closed. Early reversal races fewer payouts; late reversal never takes money back from a seller you then exonerate. Choose against your measured win rate, not your optimism — and remember Stripe's cross-border caution about retransferring after won disputes before deciding to be aggressive.
Dial 4 — floors and thresholds
Recovery is not free: each finding costs verification time, a reversal call, and often a human conversation. A floor policy — absorb findings below $N, recover above — trades small certain losses for attention on large ones. The number should come from your own costs, modeled honestly: if handling one recovery touches ten minutes of staff time at a loaded rate of $54/hour, a $4 finding costs more to recover than it returns, while a $400 one does not. Write the floor down; revisit it when volumes or staffing change.
Encode it once, derive it everywhere
The mechanical core of a deliberate policy fits in a few lines: attach a reason to every refund, and let the dials read from it rather than from whoever wrote the call site.
const PLATFORM_AT_FAULT = new Set(['duplicate', 'platform_error', 'goodwill']);
function refundFlags(reason: string) {
const sellerFunded = !PLATFORM_AT_FAULT.has(reason);
return {
reverse_transfer: sellerFunded,
refund_application_fee: sellerFunded,
};
}
Platform-fault refunds — your duplicate charge, your pricing error, a goodwill gesture — are exactly the case where the seller did their job and should keep the money. Seller-fault and buyer-remorse refunds run the other way. The point is not that this mapping is right for every platform; it is that it exists in one reviewable place instead of being re-decided implicitly by every refund call.
[DIAGRAM: Refund decision tree]
Entry: "Refund requested" -> box "attach reason".
Branch 1 -- reason in {duplicate, platform_error, goodwill}:
reverse_transfer = false; refund_application_fee = false;
(the pairing rule forbids returning the fee without the reversal)
exit label "platform absorbs; seller whole".
Branch 2 -- reason in {seller_fault, not_as_described, buyer_remorse}:
diamond "amount > floor?" --no--> absorb silently, log finding;
--yes--> reverse_transfer = true; fee per Dial 2;
exit label "seller funded within policy".
Branch 3 -- dispute event:
on created: record exposure = transfer.amount - amount_reversed;
on closed lost: diamond "above floor and terms permit?" ->
reverse due amount (+fee return per Dial 2); else queue for netting.
Caption: every leaf names who pays. If any leaf cannot, the policy
is not finished.
The three places drift hides
A refund policy lives in three systems that age independently:
- Terms — what sellers agreed to.
- Code — what the flags actually do.
- Support macros — what agents promise angry customers.
Any two can agree while the third quietly lies. Chapter 4 suggested reading five real refunds against the terms paragraph; the systematic version is quarterly: diff the macro text, the flag logic, and the clause, and treat the first divergence as a defect with an owner. The audit of chapter 8 is the same discipline pointed at money; this one points at words.
What to check on your own platform
- Write down current answers to all four dials as they behave today, sourced from code — not intention.
- Attach a reason to every refund-creation path; count how many paths currently have none.
- Compute last quarter under two counterfactuals — all-reverse and all-absorb — so the dial choice has a price tag.
- Set and document a floor with the arithmetic that justified it.
- Diff terms, flag logic, and support macros; file the divergences.
- Put the quarterly diff on a calendar owned by someone with authority over both engineering and support.
Sources: docs.stripe.com/api/refunds/create · docs.stripe.com/api/fee_refunds/create · docs.stripe.com/connect/disputes · docs.stripe.com/connect/destination-charges