Skip to content
All posts

Marketplace refund UX versus platform economics

Every generous refund flow is funded by someone specific. Quantified scenarios make the subsidy visible before you ship the policy.

FeeGuard14 min read

Marketplace refund UX versus platform economics

A buyer-friendly refund flow feels free until someone asks which balance funded it. This article quantifies who pays under four common marketplace refund promises, using fully shown ledgers with stated assumptions, so platform teams can see the subsidy, in dollars, before the policy ships.

Generosity is an allocation decision

The buyer's outcome never varies. Under every policy in this article, the buyer receives the same dollars back to the same payment method. The only variable is which balance gets debited for it, and that variable is decided by API flags, not by your help-center copy. UX makes promises; ledgers decide outcomes.

On destination charges, the defaults decide silently: the connected account keeps the transferred funds, the platform covers the refund, and the application fee stays with the platform. A team can ship an instant no-questions refund button, congratulate itself on customer love, and never notice that the seller was made whole too, at the platform's expense. Who holds legal liability for the refund and who actually finances it are separate questions; start with how marketplaces handle refund liability if the liability side is new, then work through the funding arithmetic below. For marketplaces generally, the mismatch between intended and actual funding is the single most expensive unexamined default in Connect.

Buyer view and ledger view also never meet inside the product. The buyer's receipt shows one clean credit. The platform's balance transactions show a refund entry and, somewhere else, reversal entries. The seller's payout simply shrinks by whatever the flags took. Three audiences, three incomplete stories, and no screen that joins them into one allocation picture.

Four refund flows, four funding paths

The table below maps common UX promises to their funding mechanics. Stripe's processing fee column matters because refunds never return it; someone absorbed it at sale time and no flag gives it back.

Refund flowAPI shape and flagsFunds the buyerAbsorbs Stripe's feeCarries allocation risk
Instant no-questions full refundFull refund, default flagsPlatform balancePlatformPlatform
Refund after return inspectionSame flags, delayed until goods arrivePlatform balancePlatformPlatform, plus a fraud window while goods are in transit
50% goodwill credit, instantPartial refund amount, default flagsPlatform balancePlatformNobody returns anything; pure platform cost
Seller-initiated refund from their dashboardDirect charge refunded on the connected account, refund_application_fee unsetConnected account balanceSellerSeller; the platform keeps its fee unless it manually returns it

The fourth row needs one scope note. On direct charges, Stripe debits the connected account directly, and per the direct-charge refund rules: "Application fees aren't automatically refunded when issuing a refund." Destination-charge refunds, by contrast, are always created against the platform side, so a connected dashboard cannot quietly fund one; the mechanics differ enough that the flow catalog should name your charge type explicitly. The destination-charge refund explainer covers those mechanics in detail.

The third row deserves its own arithmetic, because nothing comes back at all. Refund $50.00 of the canonical order from the next section with default flags: the platform runs 6.80 − 50.00 = −$43.20, the seller keeps both the +$90.00 transfer and the goods, and the buyer holds merchandise plus half their money. Check: −43.20 + 90.00 + 3.20 − 50.00 = $0.00, where the final term is the buyer's net spend. A goodwill habit priced per incident reads as cheap support; summed over a quarter it reads as a discount program nobody approved.

One order, three policies, three ledgers

To price generosity honestly, run one order through three policies with every line visible. Inputs and assumptions:

  • US platform, USD, destination charge with transfer_data.destination.
  • Charge $100.00 = 10000¢; application_fee_amount = 1000 ($10.00); transferred to seller: $90.00.
  • Assume Stripe's standard US card pricing of 2.9% + $0.30 (Stripe's published pricing), so Stripe's processing fee is $3.20.
  • At sale: platform balance transactions are +$96.80 charge net and −$90.00 transfer, so the platform runs at +$6.80; the seller sits at +$90.00; Stripe has +$3.20.
  • Event: full $100.00 refund.

Policy (a), platform-absorbs: refund with default flags, no reversal, no fee return.

  • Platform: +6.80 − 100.00 = −$93.20
  • Seller: +$90.00, unchanged
  • Stripe: +$3.20
  • Buyer: paid $100.00, received $100.00, net $0.00
  • Check: −93.20 + 90.00 + 3.20 + 0.00 = $0.00

Policy (b), proportional split: refund with both reverse_transfer=true and refund_application_fee=true. Because the refund is full, the proportional rule returns everything: the reversal is 100% of $90.00 and the fee return is 100% of $10.00.

  • Platform: +6.80 − 100.00 + 90.00 − 10.00 = −$13.20
  • Seller: 90.00 − 90.00 + 10.00 = +$10.00
  • Stripe: +$3.20
  • Buyer: net $0.00
  • Check: −13.20 + 10.00 + 3.20 + 0.00 = $0.00

Read that seller line carefully. Application fee refunds compensate the connected account, never the buyer; the buyer already had their $100.00 from the refund itself. On destination charges, both-true therefore hands the seller a $10.00 windfall on top of returning their transfer — compensation you chose, priced into the policy, whether or not anyone priced it. The machinery behind those two lines is observable as it runs: the reversal surfaces through transfer.reversed and the fee return through application_fee.refunded, both among the refund-related events, which is what makes this ledger reconstructible from your own data rather than taken on faith.

Policy (c), seller-absorbs: refund with reverse_transfer=true and the fee kept, executed either by that flag or by a manual call to the transfers reversals endpoint:

curl "https://api.stripe.com/v1/transfers/tr_123/reversals" \
  -u "$STRIPE_SECRET_KEY:" \
  -d "amount=9000"

The reverse_transfer flag and this call pull the same lever. Ledger:

  • Platform: +6.80 − 100.00 + 90.00 = −$3.20
  • Seller cash: 90.00 − 90.00 = $0.00
  • Stripe: +$3.20
  • Buyer: net $0.00
  • Check: −3.20 + 0.00 + 3.20 + 0.00 = $0.00

The seller's cash returns to zero, but $90.00 of merchandise walks back through their door. If those goods cannot be resold, the seller's economic position is −$90.00 in cost basis even though the cash ledger nets flat. That is what "seller absorbs" means materially: the seller financed the entire product side of the refund, and the platform's remaining loss is exactly the processing fee Stripe kept.

Position after full refund(a) platform-absorbs(b) split both flags(c) seller-absorbs
Buyer cash$0.00$0.00$0.00
Platform−$93.20−$13.20−$3.20
Seller cash+$90.00+$10.00$0.00
Stripe keeps+$3.20+$3.20+$3.20
Sum$0.00$0.00$0.00

Three legitimate policies, ninety dollars of spread between their platform costs. Nothing in the buyer's experience differs across the three columns.

The hidden subsidy map

With default flags, whichever party intends the generosity, the other may be funding it. State the directions explicitly:

  • Platform intends generosity, ships defaults: the platform pays $100.00 per full refund and keeps its $10.00 fee, but the seller keeps $90.00 of transferred funds per refunded order. The subsidy flows platform-to-seller, invisibly, on top of the buyer refund everyone sees.
  • Platform protects itself, enables reversals everywhere: the funding reverses onto sellers who shipped real goods, while the platform nets only −$3.20. Buyers stay whole; seller trust absorbs the difference.
  • Instant goodwill credits with defaults: the buyer keeps the goods and half the money, the seller is untouched, and the entire cost is the platform's — $50.00 out on the canonical order, cushioned slightly by the retained fee.
  • Direct-charge marketplaces letting sellers refund: the seller funds the refund and the platform quietly retains its fee every time, unless the platform builds the manual fee-return step.

Inspection-heavy verticals carry a second-order version of this: on vacation rental platforms, deposits and damage claims mean the refund decision arrives late, after both parties have made plans around the money, so whoever the flags pick as funder feels it as a surprise rather than a policy. And the uncomfortable honesty note: neither party's dashboard shows this split as a number. The platform sees a refund entry here and reversal entries there; the seller sees a payout that shrank. Query tools answer what happened, not what is missing, so the subsidy stays invisible until someone joins the objects by hand.

You can build that join yourself for any period: list the refunds, attach each refund's transfer reversal and fee refund records, and compute expected reversals with one stated convention — expected = round((amount_refunded ÷ charge.amount) × transfer.amount) — then diff expectation against what exists. The diff, per seller and per month, is the subsidy map in dollars, and it is the same computation any refund-liability review starts from when platforms argue about who owed whom.

When refunds and disputes collide

Generosity has a failure mode on bank-debit payment methods, where the buyer's dispute and your refund can both pay out. The refunds documentation states it plainly:

For bank debit payment methods such as SEPA Direct Debit, Bacs Direct Debit, ACH Direct Debit, ACSS (Canadian PADs), AU BECS Direct Debit, and NZ bank account debits, there's a risk of double refund. If you proactively issue a refund while the customer's bank also initiates a dispute, the customer might receive two credits for the same transaction.

Bank-debit disputes surface days or weeks after settlement, long past the window where support remembers the order. The reflexive fix — refund immediately, investigate later — is precisely the sequence that double-credits the buyer, and the later lost dispute debits the platform a second time. On a destination charge that second hit is structural: Stripe debits both the disputed amount and the dispute fee from the platform balance per the Connect dispute rules, so the same account pays twice for one unhappy buyer. Stripe's own failure data encodes the collision: among refund failure reasons sits charge_for_pending_refund_disputed, described as a customer disputing the charge while the refund was pending, with the guidance to accept or challenge the dispute "instead of refunding to avoid duplicate reimbursements to the customer."

The mitigation is a gate, not a vibe: treat charge.dispute.created as a stop signal and check for an open dispute before any refund fires.

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "");

async function safeRefund(chargeId: string): Promise<void> {
  const charge = await stripe.charges.retrieve(chargeId);

  if (charge.dispute === null) {
    await stripe.refunds.create(
      { charge: chargeId },
      { idempotencyKey: "refund-full-" + chargeId },
    );
  }
}

The idempotency key is not decoration; keys are honored for 24 hours on POSTs per Stripe's idempotency guarantees, so a retried support action cannot mint a second refund while the first settles.

Failed refunds are a UX surface

A refund policy is not shipped until its failure states are. Refund objects move through five statuses — pending, requires_action, succeeded, failed, canceled — per the Refund object reference. Each non-terminal state implies a support obligation:

Status or fieldWhat it meansOwed to the buyer
pending (cards, insufficient available balance)Refund waits for your balance to fund itHonest "processing" messaging, not "done"
requires_actionSome methods need the customer to submit bank details before Stripe can pay themWatch the expiration; nudge the customer
failedBank could not process; failure_reason says whyAn arranged alternative path
canceledYou withdrew it; treated as a failure variantAn arranged alternative path

When a refund fails, the bank returns the funds and Stripe adds them back to your balance, a process that can take up to 30 days; the object carries failure_reason and failure_balance_transaction, and a refund.failed event fires so automation can react (refund events). Waiting states carry structured detail too: pending_reason reports processing, insufficient_funds, or charge_pending, so queues can be triaged programmatically instead of guessed at. Two Connect-specific facts belong in every runbook. First, refunds go only to the original payment method, so "send it to a different card" is not a recovery option; the documented obligation is that you "need to arrange an alternative way to provide your customer with a refund." Second, for platforms on destination charges, funds from both failed and canceled refunds deposit to the platform account's Stripe balance — the money comes back to whoever funded the attempt, which is one more reason the funding-path map above matters.

Build retries as idempotent operations, route requires_action expirations into support queues, and treat failure_reason as structured data feeding your buyer communications rather than a log line.

Price your generosity on purpose

Section three produced unit costs; volumes turn them into budgets. For each policy, monthly platform cost is:

cost_per_month = R_full × U_full + R_partial × U_partial + R_goodwill × U_goodwill

where R terms are your own monthly refund counts by type and U terms are the per-event platform costs derived above: U_full is $93.20 under policy (a), $13.20 under (b), and $3.20 under (c). Partial refunds scale linearly through the same proportional machinery, so each policy has a partial unit cost as well. Refund $40.00 of the canonical order — 40% — and the platform lands at 6.80 − 40.00 = −$33.20 under (a), at 6.80 − 40.00 + 36.00 − 4.00 = −$1.20 under (b), and at 6.80 − 40.00 + 36.00 = +$2.80 under (c). Each of those columns also sums to zero once the seller (+$90.00, +$58.00, +$54.00 respectively), Stripe's +$3.20, and the buyer's net −$60.00 are counted. Goodwill credits cost their face amount minus whatever flags return.

One filled row, labeled as an illustration with an assumed volume, not a benchmark: assume 400 full refunds per month.

  • Policy (a): 400 × 93.20 = $37,280.00 per month
  • Policy (b): 400 × 13.20 = $5,280.00 per month
  • Policy (c): 400 × 3.20 = $1,280.00 per month

The gap between (a) and (c) is $36,000.00 per month, and it decomposes exactly: 400 × $90.00, the unreversed transfers. Plug your real refund counts into the formula and the right policy usually picks itself; what remains is writing the flags into the policy document so engineering and support implement the same intent, a discipline laid out in the refund policy design playbook. Ticketed businesses should run the same math per segment, because ticketing and event platforms face cancellation waves that turn a per-event goodwill habit into a seasonal line item.

Frequently asked questions

Does any of this change what the buyer receives?

No. Every policy above returns identical cash to the buyer through the identical rail — the original payment method. Only the funding leg moves between platform, seller, and Stripe. That separation is why refund UX reviews keep passing while the economics underneath diverge by ninety dollars per order.

Do I get Stripe's processing fee back when I refund?

No. Per Stripe's refunds documentation: "Stripe's processing fees from the original transaction aren't returned." In the seller-absorbs ledger, the platform's residual −$3.20 is exactly this fee. No flag combination recovers it, which is why all three policies sum to zero only when Stripe's $3.20 is counted on the other side.

What happens if the connected account cannot cover the reversal?

On a destination charge, if the refund request bundles a transfer reversal and the connected account lacks funds, the API returns an error rather than creating a pending refund. Reversals in the separate-charges pattern similarly require an available balance to succeed. Platforms then reconcile by reducing later transfers, reversing once the balance funds, or accepting the loss under their responsibility settings; where the platform enables external-account debits for negative balances (debit_negative_balances=true, supported in regions including the US, Canada, Australia, New Zealand, and SEPA countries), Stripe may also attempt to pull the deficit from the seller's bank account per Stripe's balance documentation.

How do I find out which policy my code actually shipped?

Read recent Refund objects. A populated transfer_reversal field means the destination transfer moved back; application_fee.refunded events mean fees were being returned; neither, alongside steady refunds, means default-flag behavior — the platform-absorbs policy. Comparing observed flag patterns against the written policy takes minutes and settles arguments that design documents cannot. Repeat the comparison quarterly: policies drift one hotfix at a time, and the flag that silently disappeared in a refactor will not announce itself until a seller asks why their payout shrank.

Check your own last 90 days

FeeGuard exists because this arithmetic runs silently on every refund your platform issues. The free audit reads your last 90 days of Connect activity through a restricted, read-only API key and reports every unreversed transfer, unreclaimed application fee, and uncovered dispute loss with the amounts attached. You get the answer first; ongoing monitoring is optional afterward.

Run the free 90-day audit.

FeeGuard is an independent product and is not affiliated with, endorsed by, or sponsored by Stripe, Inc.