Refunds, disputes, and chargebacks feel like back-office plumbing until the first time a SaaS crosses Visa's 0.9% dispute ratio threshold and lands on a monitoring program. At that point they become an existential problem — fees escalate, processing agreements get reviewed, and in bad cases accounts close. SaaS is uniquely exposed here because the product is intangible, subscriptions auto-renew, and friendly fraud now accounts for up to 70% of credit-card losses. This post covers the refund policy design that keeps disputes rare, the evidence package that wins the ones that happen, the dunning pattern that recovers revenue without generating new disputes, and the line between a refund and a chargeback that every finance team needs to draw explicitly.
The dispute landscape in 2026
A few numbers frame the problem. Industry-average dispute ratio for SaaS runs 0.5–1.0%; anything under 0.5% is excellent. Merchants contest roughly 54% of chargebacks they receive, and issuing banks side with them about 46% of the time — meaning the base win rate for a merchant who does nothing special is around 25% of all chargebacks initiated. AI-assisted evidence packaging lifts that number materially, but only if the underlying signals exist. The strategic takeaway: investment in prevention pays 3–5x more than investment in winning disputes. A refund costs $0 on top of the refunded amount; a chargeback costs the refunded amount plus a $15–25 fee plus staff time plus reputational risk with your processor.
The fraud-refund trap: refunding suspected fraud without closing the underlying account creates a new attack surface. The fraudster gets a refund, charges again on the same card, and now your dispute ratio includes both charges. Refund only after the account is locked and the risk signal is investigated.
Refund policy — design it before you need it
Most SaaS refund disasters trace to having no written policy, which means every refund request is negotiated individually, which means support staff default to saying no, which means frustrated customers escalate to their bank. A clear policy written once prevents dozens of disputes per quarter. Three questions to answer explicitly in a published policy:
- What qualifies for a refund? Pro-rated refunds for annual plans within the first N days; no refunds for monthly plans after the charge posts; exceptions for service outages. Write these down.
- What is the window? 14 days is the conventional SaaS default. Some products offer 30. Shorter than 7 tends to trigger disputes; longer than 30 complicates revenue recognition.
- Who can approve what? Support can refund up to $X without escalation. Finance signs off above that. Never make a customer chase multiple people for a small refund.
Partial refunds and the math
Partial refunds are more common than full ones in SaaS, and they are the place the accounting gets interesting. A customer on an annual plan cancels mid-term. Depending on the written policy, the company might refund the unused pro-rata portion, refund the last month only, or refund nothing. Whatever the rule, the ledger has to match — Stripe refunds create a credit on the original charge, which needs to reverse deferred revenue for the pro-rated period and leave the already-recognized revenue intact. Getting this wrong is the single most common cause of a SaaS ASC 606 mess.
// A refund handler that treats Stripe as one gateway and ledger as the source of truth
import Stripe from "stripe";
import { db } from "./db";
const stripe = new Stripe(process.env.STRIPE_KEY!);
interface RefundRequest {
chargeId: string;
amount?: number; // undefined = full refund
reason: "requested_by_customer" | "duplicate" | "fraudulent";
initiatedBy: string; // support agent id
}
export async function refundCharge(req: RefundRequest) {
// 1. Load the charge and its invoice to find the revenue schedule
const charge = await db.charges.findByStripeId(req.chargeId);
if (!charge) throw new Error("Unknown charge");
if (charge.refundedAt) throw new Error("Already refunded");
// 2. Never refund a charge tied to an open dispute
const openDispute = await db.disputes.findOpenByCharge(charge.id);
if (openDispute) throw new Error("Open dispute — do not refund");
// 3. Close any access the refund invalidates BEFORE issuing
if (req.reason === "fraudulent") {
await db.users.lockAccount(charge.userId, "suspected_fraud");
}
// 4. Issue the Stripe refund — idempotency keyed on charge id + amount
const refund = await stripe.refunds.create(
{
charge: req.chargeId,
amount: req.amount,
reason: req.reason,
metadata: { initiatedBy: req.initiatedBy, chargeId: charge.id },
},
{ idempotencyKey: `refund_${charge.id}_${req.amount ?? "full"}` },
);
// 5. Mirror to the internal ledger — deferred revenue reversal for unused period
await db.ledger.recordRefund({
chargeId: charge.id,
stripeRefundId: refund.id,
amount: refund.amount,
reversesDeferredRevenue: calculateUnusedPortion(charge, refund.amount),
initiatedBy: req.initiatedBy,
});
return refund;
}Three things the snippet does that hand-rolled refund code usually skips. It refuses to refund a charge with an open dispute — issuing a refund while a chargeback is in flight can create a double recovery and is explicitly against card-network rules. It locks the account before refunding when fraud is the reason, closing the attack loop described earlier. And it writes an idempotency key so retries do not create duplicate refunds.
Dispute evidence — the package that wins
When a dispute lands, the merchant has 7–21 days to respond depending on the network and reason code. SaaS is hard because the product is intangible — there is no delivery photograph, no tracking number, no signed receipt. The evidence package substitutes digital proof of agreement and use.
- Proof of authorization — IP address, timestamp, user agent, and the exact text the customer agreed to at signup. A rendered copy of the checkout page at the time of sale is ideal.
- Proof of delivery — account creation log, email confirmation with open/click timestamps, and a trail of login events from the customer's IP range.
- Proof of use — feature usage logs, API call counts, seat-level activity. For a chargeback on month six of a subscription, five months of active use is strong evidence against 'I never agreed to this.'
- Cancellation policy — screenshots of the cancel flow, evidence the customer did not use it, and the published refund policy with its effective date.
- Communication history — support tickets, emails, and any exchange where the customer acknowledged the charge or the subscription.
Build the evidence bundle automatically. A cron job that generates a signed PDF for every disputed charge — with IPs, timestamps, usage logs, and policy text — removes the variability of a human assembling the package under deadline pressure and is the single highest-ROI piece of dispute tooling.
Stripe vs direct bank acquirers
Stripe abstracts most of the dispute mechanics, including Smart Disputes which auto-packages evidence for eligible cases. Direct bank acquirers — the model most regional gateways use — leave more of the work to the merchant. The practical differences matter:
| Topic | Stripe | Direct bank / regional acquirers |
|---|---|---|
| Evidence submission | Single API call with structured fields; Smart Disputes auto-compiles | Often manual via dashboard; file uploads per dispute |
| Response deadline | Exposed clearly in API and dashboard | Varies; often shorter in practice due to processor delays |
| Win-rate visibility | Reason-code breakdowns available in Sigma | Usually requires manual tracking in a spreadsheet |
| Fee structure | $15 per dispute, refunded on win | Flat $15–25 plus case fees; sometimes not refunded on win |
| Monitoring program risk | Visible threshold alerts in dashboard | Opaque until a warning letter arrives |
Dunning — the silent chargeback generator
Subscription renewal failures are a normal part of SaaS — expired cards, insufficient funds, issuer declines. How they are handled is the single biggest controllable lever on chargeback rate. Aggressive dunning (five retries in two weeks, threatening emails, access cut off immediately) recovers cash short-term and raises dispute rates long-term as customers who thought they had canceled see repeated charges and dispute them. Restrained dunning (three to four retries spaced over ten days, clear emails, graceful access degradation) recovers 70–80% of the revenue with a fraction of the dispute cost.
- Retry on a Stripe-recommended schedule (day 1, 3, 5, 7) — not faster, not longer than 10 days.
- Email the customer before each retry with a one-click update-card link; the majority of recoveries happen from this, not the retry itself.
- Degrade access gracefully — read-only mode, export allowed, no abrupt lockout. Abrupt lockouts correlate strongly with disputes.
- Stop after the final retry. Collection dunning on a disputed-intent customer is a chargeback waiting to happen.
Metrics worth tracking
Four numbers belong on a payments dashboard and nowhere else: dispute ratio (disputes as a percentage of transactions, by month), win rate (successful responses as a percentage of contested disputes), refund rate (refunded amount as a percentage of gross revenue), and involuntary-churn recovery rate (dunning recoveries as a percentage of failed renewals). Looking at them monthly surfaces problems early; looking at them quarterly finds them after the processor has already sent a warning letter.
Key takeaways
- A written refund policy prevents more disputes than any evidence package wins. Publish it, train support on it, refund quickly within its bounds.
- Never refund a charge with an open dispute, and lock accounts before refunding suspected fraud. The fraud-refund trap is real.
- Automate the dispute evidence bundle — IPs, timestamps, usage logs, policy text — so humans are not assembling it under deadline.
- Dunning policy is a chargeback lever. Retry on a paced schedule, email well, degrade gracefully, and stop cleanly.
- Track dispute ratio, win rate, refund rate, and involuntary-churn recovery monthly. Crossing 0.9% dispute ratio is a processor-relationship problem, not a finance-team problem.