Skip to content
All posts

Take-rate erosion: the mechanics

Each mishandled refund destroys multiples of the margin that sale earned. Here is the compounding math, derived from one worked model you can adapt.

FeeGuard14 min read

Take-rate erosion: the mechanics

How much margin does mishandled refund handling cost a Stripe Connect platform, and how quickly does it compound? This article derives the answer from one worked unit economics model. Founders, finance leads, and platform engineers running Connect platforms can swap every stated assumption for their own numbers and rerun the identical arithmetic.

Take rate, defined mechanically

Take rate is kept platform revenue divided by processed volume over a window. Nothing about it is abstract: it is a division you can run on your own ledger. To keep every later number checkable, this article fixes one assumption set and never varies it silently.

The assumptions:

  • US platform, USD, card payments only.
  • Card pricing is Stripe's standard US card pricing of 2.9% plus $0.30 per transaction, per Stripe's published pricing.
  • Average charge: $100.00, which is 10,000 cents.
  • Application fee: $10.00 per sale, set through application_fee_amount.
  • Charge pattern: a destination charge created with transfer_data[destination], so the seller receives a transfer and the platform retains a fee.

The derivation, step by step:

Processing fee:   2.9% x $100.00 + $0.30 = $2.90 + $0.30 = $3.20
Seller transfer:  $100.00 - $10.00       = $90.00
Platform keeps:   $100.00 - $3.20 - $90.00 = $6.80
Gross take rate:  $6.80 / $100.00        = 0.068 = 6.8%

Six dollars and eighty cents kept per hundred dollars processed: a 6.8% gross take. The word gross is doing work in that sentence. This ratio assumes every sale sticks, and the remainder of this article measures exactly what refund handling subtracts from it.

The unit transaction before refunds

One healthy destination sale produces three balance movements and one buyer payment. The table shows the complete ledger row that the rest of this article builds on; every later figure is this row plus refund entries.

MovementPlatformConnected accountStripeBuyer
Charge settles ($100.00 less $3.20 processing)+$96.80—−$3.20−$100.00
Transfer to seller−$90.00+$90.00——
Net position+$6.80+$90.00+$3.20−$100.00

Conservation check across the net row:

+$6.80 + $90.00 + $3.20 - $100.00 = $0.00

Money moved; none appeared or vanished. The platform's $6.80 is the numerator of the 6.8% take rate, earned one sale at a time. Multiplied by volume it becomes the month: at 1,000 sales, $6,800 of gross take. Hold that figure, because the compounding section spends it.

A timing note belongs here so it does not contaminate later sections. The $96.80 does not become spendable the instant the charge settles: charge amounts land in pending, become available on a rolling schedule, and payouts reduce available (Payouts). Availability schedules change when you can spend the margin, never how much of it exists. Erosion is the opposite kind of event: it changes the amount itself, which is why it needs its own arithmetic instead of a line on a payout reconciliation.

What one default-flagged refund destroys

Refund behavior on destination charges has a documented default: the destination account keeps the transferred funds, the platform covers the refund, and the application fee is kept unless you ask for it back (Stripe's destination-charge refund documentation). A refund call that passes no flags therefore does three things at once: debits the platform $100.00, leaves the $90.00 transfer untouched, and leaves the $10.00 fee collected.

Here is the same sale with that refund added:

MovementPlatformConnected accountStripeBuyer
Healthy sale (net, from above)+$6.80+$90.00+$3.20−$100.00
Full refund, defaults−$100.00$0.00$0.00$0.00
Cumulative−$93.20+$90.00+$3.20$0.00

Conservation still holds, which is the point:

-$93.20 + $90.00 + $3.20 + $0.00 = $0.00

The buyer is whole. Stripe is paid. The seller has been paid twice in effect, and the platform carries the entire shortfall. Now the ratio this article exists to establish. The clean sale earned $6.80. The mishandled refund turned that same sale into a −$93.20 position. How many clean sales' margin does it take to refill a hole that deep?

Hole depth:            $93.20
Margin per clean sale: $6.80

$93.20 / $6.80 = 13.7   (13.705..., rounded to one decimal)

One mishandled refund erases the margin of roughly fourteen clean sales: 13.7, exactly. Not one sale's worth, and not merely the refunded sale: thirteen point seven of them. That multiple is why refund handling dominates take-rate erosion even at refund counts that look small next to total volume.

To be precise about what the default is: not a malfunction. Stripe documents leaving transferred funds with the destination account as the expected behavior, and some platforms want precisely that, for example where the seller bears refunds commercially. The API is indifferent to intent. Unless the flags say otherwise, it moves exactly the dollars tabulated above, and your policy document, not Stripe's, decides whether those are the right dollars.

Contrast the deliberate version. Passing reverse_transfer=true instructs Stripe to pull the $90.00 back through a transfer reversal, leaving:

$6.80 - $100.00 + $90.00 = -$3.20

Negative $3.20 is not leakage. It is the processing fee, which Stripe states plainly: "Stripe's processing fees from the original transaction aren't returned" (Refunds). The gap between the two behaviors is pure handling:

-$93.20 - (-$3.20) = -$90.00

Ninety dollars per mishandled full refund: the unrecovered transfer and nothing else. Note that both settings keep the $10.00 application fee, so the fee cancels out of this comparison entirely. That $90.00 figure is the unit of erosion for everything that follows.

Compounding across a book of sales

One refund is arithmetic; a book of them is finance. Scaling the model requires these adjustable assumptions. They describe a shape, not a benchmark, and every one of them is meant to be replaced with your own operating numbers:

  • N = 1,000 sales per month at a $100.00 average charge.
  • Application fee $10.00 and processing $3.20 per sale, as derived above.
  • r = full refunds per month. The grid below tries 30, 60, and 120.
  • q = the share of those refunds issued with default flags, meaning the transfer reversal is missing. The grid tries 0.5 and 1.0.
  • Incremental cost per default-flagged refund versus a correctly flagged one: $90.00, the unrecovered transfer derived in the previous section.

One policy decision sits inside that final bullet and deserves stating outright. On destination charges, refund_application_fee defaults to false, and the correctly flagged comparison used here (reverse_transfer=true, fee kept) also keeps the fee. Because both branches retain the $10.00, the fee nets out of the comparison and the incremental cost isolates the transfer. If your refund policy returns the fee, both branches shift by the same $10.00 and the $90.00 gap between them survives unchanged.

The annual formula follows directly from the unit cost:

Extra annual cost = r x q x $90.00 x 12

Worked cell, r = 60 and q = 0.5:

60 x 0.5 x $90.00 x 12 = 30 x $90.00 x 12 = $32,400

Every cell in the table below is computed the same way:

Refunds per month (r)Default-flagged share (q)Extra annualized costShare of assumed annual take
300.530 × 0.5 × 90 × 12 = $16,20019.9%
301.030 × 1.0 × 90 × 12 = $32,40039.7%
600.560 × 0.5 × 90 × 12 = $32,40039.7%
601.060 × 1.0 × 90 × 12 = $64,80079.4%
1200.5120 × 0.5 × 90 × 12 = $64,80079.4%
1201.0120 × 1.0 × 90 × 12 = $129,600158.8%

The denominator is the model's gross annual take, derived rather than asserted:

1,000 sales x $6.80 x 12 months = $81,600

Each percentage divides the cost cell by that figure. For the worked cell: $32,400 ÷ $81,600 = 0.397, which rounds to 39.7%. Read the middle row slowly. At sixty refunds per month with half of them mishandled, refund handling consumes about two-fifths of everything the take rate earns, while the revenue dashboard reports an ordinary month. In the top right of the table the book gives back more to handling than it keeps from selling.

Normalize the model if your mix differs from the assumed one. Extra cost scales linearly in both r and q, so erosion as a share of processed volume is r × q × $90 divided by N × $100 per month. Worked: 60 × 0.5 × $90 = $2,700 monthly, against $100,000 of monthly volume (1,000 sales × $100.00), so $2,700 ÷ $100,000 = 0.027 — a 2.7% haircut to gross take before annualizing either side. A platform running a thinner take than the modeled 6.8% feels every point of that haircut harder, which is the whole reason to rerun the grid with your own fee, ticket size, and refund counts.

This is a controller and CFO line item before it is an engineering one; the reconciliation duty lands on finance (what that role owns). Mechanically, every dollar in the table traces back to one missing flag on a destination-charge refund.

Why dashboards miss the leak

Revenue is booked when the charge settles. The refund posts too, as a visible −$100.00 entry. What never posts is the problem: there is no reversal entry that should have existed, and no fee-refund entry that should have accompanied it. A dashboard reconciles entries against entries, and absence generates no row to reconcile against.

Nothing fails, either. Stripe events announce occurrences, such as refund.created and charge.refunded (the refund events list), and those fire normally on precisely the refunds described above. No error class exists for the reversal you did not request. Webhooks arrive, handlers acknowledge them, and the pipeline stays green while the balance quietly absorbs the shortfall.

Delivery mechanics deepen the silence rather than breaking it. Webhook endpoints receive events at least once, with retries stretching up to about three days (Webhooks), and handlers verify signatures and stay idempotent. All of that transport machinery works perfectly on these refunds, because nothing was ever sent for it to deliver. A clean delivery log is evidence about transport, not about reversals.

Query tools inherit the same limitation. Sigma and Stripe reporting answer the question of what happened across your objects (Sigma); the question here is what is missing, and no single table contains that. Detection requires joining refunds to charges to transfers to reversals, computing the expected reversal for each refund, and diffing. The Dashboard's Collected fees view completes the illusion, since a $10.00 fee sitting undisturbed looks exactly like revenue performing well.

Doing nothing is itself a policy choice, and the table above prices it (further reading on that baseline). The alternative is measurement, and the formula turns out to be short.

Bounding your own erosion

Measure your own book rather than borrowing anyone's averages. Over any trailing window, two sums bound what mishandling costs and how much of it is recoverable:

Expected reversal = round((amount_refunded ÷ charge.amount) × transfer.amount) Missing transfer = Expected reversal − Σ(existing reversal amounts)

The fee side has a mirror image, substituting the application fee amount for the transfer amount:

Missing fee = round((amount_refunded ÷ charge.amount) × application_fee_amount) − Σ(existing fee-refund amounts)

Here is a worked pass over three hypothetical findings, all against the canonical sale from earlier (charge 10,000¢, transfer 9,000¢, fee 1,000¢):

Finding A: $40.00 refund, no reversals found
  4,000 / 10,000      = 0.40
  round(0.40 x 9,000) = 3,600 cents = $36.00
  Missing transfer    = $36.00 - $0.00 = $36.00

Finding B: $60.00 refund, no reversals found
  6,000 / 10,000      = 0.60
  round(0.60 x 9,000) = 5,400 cents = $54.00
  Missing transfer    = $54.00 - $0.00 = $54.00

Finding C: $100.00 refund, no reversals found
  10,000 / 10,000     = 1.00
  round(1.00 x 9,000) = 9,000 cents = $90.00
  Missing transfer    = $90.00 - $0.00 = $90.00

Summing the findings:

$36.00 + $54.00 + $90.00 = $180.00

That $180.00 is a hard floor: money that a reversal schedule can pull back from sellers, independent of any estimate or model. Erosion beyond the floor continues in the fee column and on the dispute side; the floor is where recovery starts, not where the damage ends. Run the same two formulas over your own trailing ninety days of refunds and the paragraph above becomes your balance sheet, in dollars, with no assumptions left to argue about.

The same computation as code, in integer cents throughout:

import Stripe from "stripe";

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

async function missingReversalCents(refundId: string): Promise<number> {
  const refund = await stripe.refunds.retrieve(refundId);
  const charge = await stripe.charges.retrieve(refund.charge as string);
  if (charge.transfer === null || charge.transfer === undefined) {
    return 0;
  }
  const transfer = await stripe.transfers.retrieve(charge.transfer as string);
  const reversals = await stripe.transfers.listReversals(transfer.id);
  const reversedCents = reversals.data.reduce(
    (sum, rev) => sum + rev.amount,
    0,
  );
  const expectedCents = Math.round(
    (refund.amount / charge.amount) * transfer.amount,
  );
  return Math.max(0, expectedCents - reversedCents);
}

missingReversalCents("re_EXAMPLE").then((cents) => {
  console.log(cents);
});

Point the function at any destination-charge refund identifier. Reading the calls in order: retrieve the refund, retrieve its charge, return zero for anything without a transfer field since only destination charges carry one, pull the transfer, sum whatever reversals already exist, compute the expected reversal from the rounding rule, and return the positive difference only, so a partially reversed refund still yields a correct residual instead of a double count. Refund amounts themselves obey a hard ceiling — cumulative refunds can never exceed the original charge (Refunds create) — so the expected values stay bounded by construction. Against Finding A the function prints 3600, which reads as $36.00.

Turning a backlog of findings into executed, auditable reversals is its own procedure (bulk reversal playbook), and catching each new occurrence as it happens is the ongoing half of the same computation, which is the part FeeGuard automates once an audit establishes the baseline.

Frequently asked questions

Is take rate the same thing as gross margin?

No. The 6.8% in this article is kept platform revenue divided by processed volume, measured before refund-time losses and before your operating costs. Gross margin subtracts considerably more. Erosion attacks the take first, and margin inherits whatever the take loses.

Why 13.7 sales rather than 14?

Because the division is $93.20 ÷ $6.80 = 13.705…. Rounded to one decimal it is 13.7; rounded up for planning purposes it is fourteen clean sales to refill one hole. The decimal preserves the exact ratio, and the integer answers the operational question of how many sales you need.

Where does refund_application_fee fit into this math?

It moves a different dollar than the one eroding above. Setting it true on a full destination-charge refund pushes the $10.00 fee back to the connected account, moving the platform from −$3.20 to 6.80 − 100 + 90 − 10 = −$13.20. That is compensation to the seller, not recovery of a platform loss. Against that both-flags setting, the default-flag gap narrows to $80.00; against the reverse-only setting used in this article, it remains $90.00 (parameter reference). Fix the policy first, then read the gap.

Which time window should I scan?

Start with the trailing ninety days: long enough to smooth payout timing, recent enough to act on. If payouts lag materially in your configuration, reach further back, since the formulas work identically over any range and the floor simply scales with what the window contains. What matters is summing both formulas across the whole window rather than sampling individual refunds.

Check your own last 90 days

FeeGuard exists because this arithmetic runs silently under 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.