A refund rate on a revenue report tells you about customer expectations. A dispute rate tells you whether you are about to lose your payment processor, and only one specific way of computing it is the number the card networks actually use. This playbook computes the compliance metric on each network's own denominator, compares it against the published count and rate thresholds, and is explicit that any figure derived from a payments API is a floor rather than the networks' number.
Steps
-
Pull the dispute numerator over at least 13 months.
stripe.list_disputeswithcreated_gte/created_lte,limit: 100, paging onstarting_afteruntil exhausted. Keepamount,currency,reason,status,charge,createdandevidence_details.due_by. On Polar usepolar.list_disputes; on Dodo usedodo_payments.list_disputes. Thirteen months is the minimum because the network programmes evaluate month bands and exit conditions over consecutive months, and because the most recent months are still filling in. -
Build the denominator, split by card brand.
stripe.list_chargeshas no status filter, so either page the full window and filter client side to successful charges, or, for meaningful volume, preferstripe.searchwithresource: "charges"and a query such asstatus:"succeeded" AND created>..., paging the synthetic_next_pagerow. Keeppayment_method_details.card.brandso Visa and Mastercard can be counted separately. On Polar usepolar.list_payments, orpolar.get_metricsfor pre-computed order and revenue series; on Dodo usedodo_payments.list_payments. If the connected provider does not expose card brand on the returned rows, report a single blended rate and say plainly that VAMP and ECM thresholds are per-network and cannot be checked exactly. -
Compute dispute activity and dispute rate, each on the right denominator. Build two monthly series: disputes bucketed by the dispute's own
createddate, which gives activity, and disputes bucketed by the underlying charge'screateddate, which gives rate. Divide the Visa numerator by Visa payment count in the same calendar month, and the Mastercard numerator by Mastercard payment count in the previous month. Report activity as the compliance number and rate as the diagnostic number, and label which is which on every line. -
Compare against the published thresholds and the slope. Visa VAMP: non-compliant at a VAMP count of 5 and a 0.5% ratio, excessive at 1,500 count (150 in CEMEA) and 1.5% ratio (2.2% CEMEA), where the VAMP count includes TC15 disputes plus TC40 early fraud warnings and a transaction appearing in both is counted twice. Mastercard ECM: 100 to 299 chargebacks and a 1.5 to 2.99% rate, with fines from month two at 1,000 USD rising to 100,000 USD by month 19 and beyond. Mastercard HECM: 300 or more and 3%. Mastercard exits require being under threshold for three consecutive months. Also check the industry line that dispute activity above 0.75% is excessive, and report month-over-month slope, since Stripe states a sudden spike or steep upward trend can trigger placement before the threshold is reached.
-
Compute refund rate separately and attribute both to products.
stripe.list_refundsover the same window, paged fully, joined to charges viachargeorpayment_intentfor refund rate by count and by volume, and to split full from partial refunds. On Polar usepolar.list_refunds, noting that itsnet_revenuemetric already nets refunds so you must not subtract them twice; on Dodo usedodo_payments.list_refunds. Then join disputed charges throughstripe.list_invoicesto subscription, price and product, usingstripe.list_productsandstripe.list_pricesfor names andstripe.searchwith a metadata query where product identity lives in metadata. Group disputes by reason code: a cluster of general or duplicate reasons usually points at an unrecognised statement descriptor or a confusing billing statement rather than at fraud. -
Size the cash impact and the economics of fighting.
stripe.list_balance_transactionswithtype: "adjustment"captures the actual cash movement including dispute fees, which are a separate event from the disputed amount.stripe.get_balancereveals reserves or held funds, which is often the first visible symptom of a processor review. On Dodo,dodo_payments.list_ledger_entrieswithevent_typein dispute, dispute_reversal, dispute_fees and ethoca_fees gives the same picture, and itsusd_equivalent_amountis the only built-in multi-currency normaliser across the three providers. For each dispute compute expected value of contesting as win probability multiplied by disputed amount, minus the dispute fee and the effort cost, rather than advising that every dispute be fought. Where charges and disputes are mirrored to a warehouse, callpostgres.get_schemathenpostgres.queryinstead of paging thousands of API records. -
Report. One row per month with columns: month, successful payments (Visa), successful payments (Mastercard), disputes by dispute date, dispute activity %, disputes by charge date, dispute rate %, VAMP count, Visa threshold status, Mastercard ECM rate against previous-month payments, Mastercard threshold status, refunds count, refund volume, refund rate %, dispute fees and adjustments, maturity flag for months inside the 120 day window. Then deliver one judgement: the nearest threshold, the distance to it in both count and rate, the direction of the trend, and an explicit statement that the figures are a lower bound because Stripe absorbs some no-liability disputes that never appear in the API.
Gotchas
- Any API-derived dispute rate is a floor, not the networks' number. Stripe states that some disputes where you have no liability do not appear in the Dashboard or API responses because Stripe handles them on your behalf, yet the monitoring programmes still include them in their calculations. The gap is widest for merchants who refund heavily. Never present the computed rate as the compliance figure without this caveat.
- Activity and rate are different numbers and only one is the compliance metric. Stripe's own worked example produces 1.0% and 0.3% for the same week. Reporting the wrong one either panics the team or falsely reassures it. And Visa and Mastercard do not share a denominator: Visa uses the same calendar month, Mastercard uses the previous month, so a growing business reads higher on Mastercard purely from denominator lag.
- The last 120 days are provisional, and won disputes still count. Cardholders can dispute a charge up to 120 days after payment and sometimes longer, so any recent month's rate will keep moving. Stripe is explicit that all disputes, won or lost, count toward the rate, which means fighting protects cash but not compliance. Refunding does not help either: monitoring programmes do not consider refunds when identifying disputes.
- Early fraud warnings count for Visa and there is no endpoint for them here. VAMP counts TC40 early fraud warnings alongside TC15 disputes, and a transaction appearing in both reports is counted twice. No tool in this inventory returns early fraud warnings, so the computed VAMP count is structurally understated and that gap must be stated, not estimated.
- One business can be several Visa accounts. Visa identifies an account by the static component of its statement descriptor and its acquiring bank, so a multi-descriptor or multi-country merchant has several rates and none of them equals the blended one. Check whether descriptors differ before presenting a single figure.
- Count-based rates travel across currencies, volume-based rates do not. Dispute amounts are integer minor units of the original currency, so divide by 100 once and not at all for zero-decimal currencies such as JPY. Only
dodo_payments.list_ledger_entriesexposes a USD equivalent, so the count-based rate is the comparable metric and any volume total needs a stated FX source. Dispute fees and reserves are separate cash events from the disputed amount, and reporting only disputed volume understates the cost. - Low volume makes the rate meaningless, and the month cut is UTC. At 600 transactions, 8 disputes is 1.33%, so below roughly 1,000 monthly transactions a single dispute swings the rate materially: always print the count next to the rate and apply the VAMP count floor of 5 before raising an alarm. Count each dispute once against a charge, never against the invoice and the payment intent as well, since a disputed renewal has all three. Networks evaluate calendar months while Stripe filters are UTC epoch seconds, which moves the last day's charges into the wrong month for a US merchant sitting near a threshold. Filter
livemode, because test-mode disputes are trivial to create.