Portreeve
explainer · 7 Sept 202612 min read

Refund abuse in SaaS: write the policy first

Refund abuse in SaaS is usually a policy problem, not a detection one. How to bound a money-back guarantee and spot the same person on their second pass.

Refund abuse is what happens when your money-back guarantee stops working as a safety net and starts working as the price. Someone signs up, uses the product hard for eleven days, exports what they came for, and emails on day thirteen to say it wasn't for them. You refund, because that is what the guarantee says. Six weeks later a new account appears with a different email, runs the same eleven days, and sends the same email.

Stripe defines it by cost rather than intent, in its customer abuse guide: refund abuse "occurs when a customer takes advantage of your refund policy and causes disproportionate operational cost." That framing is worth keeping, because it stops you arguing with yourself about whether a specific person meant to do it.

Most founders meet this as a detection problem. Most of the fix is in the policy. The detection part only matters on the second pass, and it only works if you have stopped trusting the email address.

What a serial refunder actually does

Digital goods deliver their full value on day one. There is no shipping window, no restocking, no item to send back. Whatever the buyer extracted from the product is still theirs after the money goes home. That asymmetry is the whole mechanism, and it is why the same shape shows up across course sellers, API products, design tools, and anything with an export button.

The pattern has three beats. Consume at maximum rate inside the guarantee window. Request the refund politely, citing the guarantee you published. Return later under a new identity and repeat.

Each request survives inspection on its own. It arrives inside the window, it is worded reasonably, it names a real complaint ("slower than I expected", "not what I needed"), and it concerns one charge for $29 or $99. You have nothing to compare it to, because the only account you can see is the one in front of you. A support person clearing a queue approves it in nine seconds, correctly, given what they know.

The shape only becomes visible in aggregate, which is why Stripe's suggested approach is a Sigma query that groups charges by customer and counts full refunds per customer over a year. Read the comment on the first line of that query, though: customer_id "can be changed to another customer identifying attribute such as card, email, or shipping address." Stripe is telling you, in a SQL comment, that the choice of join key is the entire problem.

The refund you refuse becomes the dispute you lose

Before you tighten anything, price the alternatives, because the instinct to refuse a suspicious refund is expensive in a way that is easy to miss.

A refund costs you the amount plus the original processing fee. Stripe is explicit: "Stripe's processing fees from the original transaction aren't returned" when you refund a payment. At the standard US online rate of 2.9% plus 30 cents, a $29 charge you refund is $30.14 out the door.

A chargeback costs more, and the money is the smaller part of it. Here is the same $29 charge under each outcome, using the US fee schedule from Stripe's June 2025 dispute pricing update:

Outcome on a $29 chargeWhat leaves your account
You refund$30.14: the amount plus the fee you don't get back
Dispute, you accept it$44: the amount plus the $15 dispute received fee
Dispute, you counter and lose$59: $29 plus $15 received plus $15 countered
Dispute, you counter and win$15: the received fee, since the countered fee comes back

Stripe returns the countered fee if you win and, per the dispute fee rules, "unless otherwise stated in your Stripe contract, we never return the dispute received fee." Winning is the good outcome and it still costs $15.

The ratio hit is the part that should decide this for you. Card network monitoring programs "don't consider refunds when identifying disputes" and "don't consider dispute outcomes," per Stripe's monitoring programs documentation. A dispute you win counts exactly the same as one you lose. Visa's VAMP sets its non-compliant thresholds at a count of 5 and a ratio of 0.5%, where the count sums disputes from TC15 reporting and fraud reports from TC40 early fraud warnings, and a transaction appearing in both "will be counted twice."

Five is a small number. Capture 400 payments in a month and five disputes puts you over both non-compliant lines at once. You will not reach Visa's excessive tier at that size, because outside CEMEA that also takes 1,500 disputes, but non-compliant is the tier where Visa "may assess fees" and where the remediation conversation starts. Low volume does not protect you. It makes the denominator small.

So the arithmetic on refusing a refund to someone you suspect is a serial refunder: you save $30 if they go away, and you pay $44 plus a mark on a monthly ratio if they open their banking app instead. The refuse-and-fight strategy is a bad trade at small ticket sizes, and it stays bad even when you are right about the person. Winning the friendly fraud dispute does not un-count it.

Write the refund abuse policy before you write the detector

The common advice at this point is to drop the money-back guarantee. That is the wrong lever. The guarantee converts, it converts most for the buyers who are least sure of you, and removing it costs you real revenue to stop a minority of requests.

Bound it instead. A guarantee is a promise about dissatisfaction, and nothing stops you from saying so in the terms.

Three bounds do most of the work.

A window, stated in days. Fourteen is what the merchants of record work to, because consumer law sets the floor. Paddle's refund policy honours a 14-day statutory right to withdraw for EU, EEA, Swiss and UK consumers, a 7-day unconditional right to cancel in South Korea, Brazil, China and Canada, and 5 days in Singapore. If you sell to consumers in those regions the statutory part is not yours to remove, so pick your window at or above the floor and stop treating it as negotiable after the fact.

A usage ceiling, stated in your units. This is the bound almost nobody writes and the one that separates dissatisfaction from consumption. For an API product: full refund within 14 days, provided less than 10% of the plan's included calls have been used. For a tool with an export: within 14 days, before the third export. Publish the number. A buyer who reads it and leaves was going to refund anyway.

One refund per customer, not per account. The second request from the same person meets a different policy, and that policy can be "we'll cancel and prorate" rather than "we'll refund in full."

Paddle also states that refunds "will not be issued where there is evidence of fraud, refund abuse, or other manipulative behaviour." Copy that sentence's function, if not its wording. You want one clause that lets you decline without inventing a rationale on the day.

Publish the window and the one-per-customer rule. Keep the usage ceiling in the terms and do not build a countdown into the UI; you are writing an enforcement basis, not a taunt.

Stripe's own remedies for high-refund customers are: notify them about their refund history, "restrict future order attempts (temporary or permanent)," and "introduce a fee to deter repeat refunds." All three require you to know that the person in front of you is the same person as before. That is where the policy stops being enough.

Why per-account checks can't see this

The second signup is a clean account. New email, new Stripe Customer object, no refunds, no support tickets, no history of any kind. Every per-account check you run passes, because there is nothing on the account to fail.

The link is in the identity keys, and they are not equally hard to change. Ranked by what a returning refunder has to spend:

Email is free. Gmail dot tricks, plus-addressing, a fresh throwaway from any of a thousand disposable domains. Blocking by email is the trial-abuse lesson repeated, and it fails for the same reason: you are keying on the cheapest attribute in the transaction.

IP is nearly free and actively misleading. A VPN costs a few dollars a month, and shared egress means a per-IP count catches an office, a university, or a CGNAT range full of innocent people. IP is worth reading as a signal. It is not worth linking two accounts on by itself.

Device fingerprint costs a new browser profile at minimum, a new machine at worst. Most repeat refunders do not bother, because they are not professionals; they are one person who found a trick that worked. When they do bother, the change is visible as a change.

Card fingerprint costs a new card, and virtual card issuers have made that cheaper than it used to be. Phone costs a number.

Every one of these keys is beatable on its own. The ones people bother to beat are the ones you were relying on. Describing trial abuse on Hacker News, caydenm gave a concrete case: "a user who signed up 8 week in a row using different emails, names, IPs and cards." That person is invisible to every check keyed on one attribute and obvious to anything that links across several.

Stripe's customer abuse guide lists "many accounts created using the same card or device and browser fingerprint" as a signal for account resale. Refund abuse leaves the same trace. The device fingerprint is usually the strongest key you can collect yourself, because it survives the email change and the card change that a returning refunder makes deliberately.

Refund abuse detection starts with memory

There is no model that looks at a single polite refund request and tells you the person is a serial refunder. The information is not in the request. It is in what happened the last time, attached to something that person could not cheaply change.

So the thing to build is memory, not a score. When you approve a refund you suspect, record the outcome against the identity keys rather than the email address, and make sure the record is readable at the two moments that matter: the next signup, and the next checkout.

This is the layer Portreeve exists for. Accounts are linked across hashed identity keys, including email, device fingerprint, card fingerprint and phone, so a customer who refunds and returns under a new email lands in the same cluster; IP is a soft signal that never links accounts on its own. When you report the original event through the feedback API as confirmed_abuse or chargeback, that outcome marks the whole cluster and travels into the next signup or checkout_attempt verdict. Because a review verdict never blocks the user, the returning cluster can surface in a queue for a human to read before the next refund is approved, rather than getting declined at the door.

The verdict handling shape matters more than the flag itself. A refund history should change who reads the request, not whether the person can buy.

Whatever you use, the failure mode to avoid is the spreadsheet keyed on email. It feels like memory and it is not.

When to refund anyway

Refuse less than you want to.

First offense, small amount, ambiguous usage: pay it. You are buying out a dispute that would cost you the amount plus $15 plus a tick in a monthly ratio measured against a threshold of five.

Stripe puts a number on the same trade for early fraud warnings, and it generalises reasonably: their analysis suggests "the optimal point for issuing a refund on early fraud warnings is on charges that are roughly less than or equal to your dispute fee," and that it is "likely not worthwhile to refund EFWs on charges more than 35 percent higher than your dispute fee." At a $15 US dispute fee that is a threshold around $20. Your own threshold for refund requests should sit higher, because unlike an early fraud warning, a refund request is a person telling you directly that they are considering the alternative.

Two mechanics worth knowing before you stall.

Once a dispute is open you cannot refund outside it. Stripe: "You can't issue a refund outside the dispute process while the dispute is open." Deciding slowly removes the cheap option.

Refunding after the fact also does not clean the record. "Proactively refunding a flagged payment doesn't affect the fraud warning," and the only refund that prevents a fraud report is one processed as a reversal, "which usually happens within 2 hours of the payment capture." Speed is the only lever there, and it is a two-hour lever.

The cases to refuse are narrow: the request violates a bound you published, and you can name the bound. "Your workspace ran 340,000 API calls of the 500,000 included, which is past the 10% refund ceiling in section 4" is a decline someone can argue with on the merits. "We think you're abusing this" is a decline that becomes a chargeback.

Where this sits with the rest of your controls

Refund abuse is the tail end of the same behaviour as free trial abuse. The person who cycles trials and the person who cycles refunds are running the same play against a different part of your funnel, and often they are the same cluster: the trial ran out, so they paid, extracted, and asked for it back.

Which means you should not build a separate refund-abuse system. Decide once at signup and again at checkout, with the same linked history behind both decisions, and let the refund outcome feed back into that history rather than into a support macro nobody reads.

The policy comes first because the policy is what makes the decision defensible. The memory comes second because it is what makes the decision possible on the second pass.

If you want the linked-history part without building the identity graph yourself, Portreeve's free tier screens 1,000 events a month with no card: create an account.

← Back to all posts