Skip to main content
Designing for Chargebacks and Disputes
AI & Technology

Designing for Chargebacks and Disputes

11 min read
331 views
Share:

The first serious chargeback I ever dealt with cost us about £40,000 and, worse, taught the whole company that our dispute process was held together with a spreadsheet and one very patient person in finance named Rita. A card issuer clawed back a batch of transactions from a merchant who had, it turned out, been shipping goods weeks late. By the time we noticed the pattern, the money was gone and the representment window had closed. Rita had been tracking it all by hand.

Designing for Chargebacks and Disputes
Designing for Chargebacks and Disputes

That was the moment I stopped thinking of chargebacks as an edge case that finance handles and started treating them as a first-class part of the payment system. They are not an exception path. In any business that touches cards, disputes are a permanent, recurring flow of events that arrive on their own schedule, carry hard deadlines, move money in both directions, and generate real regulatory obligations. If you design for them from the start, they are manageable. If you bolt them on later, you will be rebuilding your ledger under pressure, which is exactly what we ended up doing.

The shape of a dispute is not the shape of a payment

Most payment systems are built around a hopeful, forward-moving story: authorize, capture, settle, done. Money goes one way, states only advance, and the happy path is the path you optimize. Disputes break every one of those assumptions. A transaction that settled cleanly three months ago can suddenly reverse. State moves backward. Money you already recognized as revenue is provisionally yanked out of your account by the card network, and you get a short window to fight for it.

The lifecycle itself is genuinely multi-stage, and each stage has its own clock. A cardholder disputes a charge with their issuer. The issuer raises a chargeback and debits the acquirer, who debits you. You can accept it, or you can represent the transaction with evidence. The issuer can accept your evidence or escalate to a second chargeback, and from there it can go to arbitration with the card scheme, which charges a fee win or lose. Card networks publish specific reason codes for all of this, and the deadlines differ by scheme and reason. Modeling this as a simple boolean "disputed" flag is the mistake almost everyone makes first, and it is the one that will hurt the most.

Model disputes as events, not as a status column

Here is the design decision I feel most strongly about: a dispute is an append-only sequence of events against a payment, not a mutable field on the payment row. The instant you try to represent a dispute as payment.status = 'disputed', you have lost the ability to answer the questions that actually matter. When did it arrive? What was the reason code? What evidence did we submit and when? Did we win the first round and lose arbitration? A single status column cannot hold that history, and auditors, banking partners, and your own future self will all want it.

I model each dispute as its own aggregate with an immutable event log. The payment references its disputes; the dispute owns its own state machine. Every transition is a timestamped, typed event with the actor and the source that triggered it. This is not gold-plating. When your acquirer emails asking why your dispute win rate dropped last quarter, you want to reconstruct exactly what happened from data, not from Slack archaeology.

CREATE TABLE dispute (
    id              UUID PRIMARY KEY,
    payment_id      UUID NOT NULL REFERENCES payment(id),
    scheme          TEXT NOT NULL,          -- visa, mastercard, amex
    reason_code     TEXT NOT NULL,          -- e.g. '10.4', '13.1'
    disputed_amount NUMERIC(18,2) NOT NULL,
    currency        CHAR(3) NOT NULL,
    respond_by      TIMESTAMPTZ NOT NULL,   -- hard network deadline
    current_state   TEXT NOT NULL,          -- projection, not source of truth
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE dispute_event (
    id            BIGSERIAL PRIMARY KEY,
    dispute_id    UUID NOT NULL REFERENCES dispute(id),
    event_type    TEXT NOT NULL,            -- opened, evidence_submitted,
                                            -- represented, won, lost, arbitration
    actor         TEXT NOT NULL,            -- system, agent:rita, network
    payload       JSONB NOT NULL,
    occurred_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- current_state is a cached read model. The event log is truth.
-- If they ever disagree, trust dispute_event and rebuild.

The deadline is the actual product

Everything about dispute handling reduces to one uncomfortable fact: there is a hard deadline, it is set by someone else, and if you miss it you lose automatically regardless of how right you were. Visa and Mastercard give you windows measured in days, and those windows start from a date on the network's clock, not yours. I have watched a team lose a perfectly winnable $8,000 dispute because the notification email landed in a shared inbox during the Christmas break and nobody looked until January.

So the single most valuable thing your dispute system does is not analytics or clever machine learning. It is making sure no deadline is ever missed silently. That means the respond_by timestamp is a first-class, indexed, monitored field. It means a background job that surfaces anything approaching its window, escalating loudly. It means treating a dispute with 48 hours left and no evidence attached as a page-worthy incident, not a task in someone's backlog.

A dispute you forget to answer is a dispute you have chosen to lose. The network does not care that you were busy, understaffed, or on holiday. The clock is the clock.

Get the ledger right before you get anything else right

Chargebacks move money, and they move it provisionally, which is where the accounting gets genuinely hard. When a chargeback lands, the funds are typically debited from you immediately, before the outcome is decided. If you win the representment, the money comes back. If you lose, it stays gone and you may owe additional fees. Your ledger has to represent "money that has left my account but might return" as a distinct state, because it absolutely is not the same as a refund and it is not the same as settled revenue.

Enjoying this article?

Get more like it in your inbox — practical engineering leadership, fintech, and AI. No spam, unsubscribe anytime.

The mistake I have cleaned up more than once is a system that simply subtracts the disputed amount from a balance and moves on. Then the merchant wins the dispute, the money returns, and now the books do not reconcile because there was no corresponding liability recorded for the provisional hold. Double-entry accounting exists precisely for this. Model the provisional debit as a real ledger entry against a "disputes in progress" liability account, and reverse it explicitly on resolution. Your reconciliation with the acquirer's settlement file should tie out to the penny, and if it does not, you have a bug you need to find before your auditor does.

Reason codes decide what evidence even matters

Not all disputes are the same fight, and treating them as one generic workflow wastes the thing you have least of: time inside the response window. The reason code tells you what the issuer is actually claiming, and therefore what evidence has any chance of winning. A "product not received" dispute is won with delivery confirmation and tracking. A "fraudulent transaction" dispute needs AVS and CVV match records, device fingerprints, and prior good history with that cardholder. A "product not as described" dispute is often not winnable at all, and knowing that lets you decide quickly whether fighting it is worth the arbitration risk.

I like to route disputes automatically by reason code into evidence templates, so the person or system assembling the response starts from the right checklist rather than a blank page. A rough version of that routing looks like this:

  • Fraud codes (Visa 10.x): pull AVS/CVV results, 3-D Secure authentication data, IP and device history, and any account age signals.
  • Product not received (13.1): pull the carrier tracking number, delivery confirmation, and the customer's confirmed shipping address.
  • Subscription cancelled (13.2): pull the cancellation policy the customer agreed to, the exact timestamps of billing, and any cancellation attempt logs.
  • Credit not processed (13.6): pull the refund record, because more often than not you already refunded and the customer disputed anyway. Accept these fast.

Deciding whether to fight is a business decision, not a reflex

There is a temptation, especially early on, to fight every dispute out of principle. Do not. Every representment costs staff time, and arbitration carries a scheme fee of roughly $500 that you pay whether you win or lose. If you are disputing a $30 transaction, the math is obvious and it is not in your favor. Build the expected-value calculation into the workflow: probability of winning for this reason code, times the amount at stake, minus the cost of fighting. When that number is negative, accept the loss and move on.

What surprised me most when we finally had clean data was how much of our dispute volume was our own fault. A confusing billing descriptor that did not match our brand name generated a steady stream of "I don't recognize this charge" disputes from customers who had genuinely bought from us. Fixing the descriptor removed more disputes than any evidence strategy ever did. The best chargeback is the one that never happens, and a lot of prevention is unglamorous product and operations work rather than anything clever in the payments stack.

The threshold nobody warns you about

Card networks run monitoring programs, and crossing their thresholds is far more expensive than the disputes themselves. Both Visa and Mastercard track your dispute ratio, and once you exceed roughly one percent of transactions, you land in a program with escalating fines and, eventually, the threat of losing card acceptance entirely. I have seen a merchant with an otherwise fine business get put on notice because a single bad product launch spiked their ratio for two months.

This changes how you monitor. It is not enough to track disputes in absolute numbers; you have to track the ratio against transaction volume on a rolling basis, and you need to see it trending before you cross the line, not after. I keep a dashboard that shows the current-month ratio by merchant and by product, with an alert well below the network threshold so we have time to react. When you are inside one of these programs, every dispute counts double, because it is both a direct cost and a data point pushing you toward existential trouble with your card scheme.

The system is only as good as the humans behind it

For all the engineering, dispute handling is still substantially an operational discipline. Someone has to gather evidence, make judgment calls, and talk to your acquirer when things get weird. The job of the software is to make those people fast and accurate, not to pretend they do not exist. The best setup I have run gave the operations team a single queue, sorted by deadline and expected value, with the evidence for each dispute pre-assembled and a clear recommendation to fight or fold that they could override.

That override matters. Automated recommendations are useful right up until a customer with a genuine grievance and a compelling story shows up, and a human recognizes it in a way the rules never would. Design for the human to be in the loop for the decisions that matter and out of the loop for the mechanical parts. When we moved from Rita's spreadsheet to that queue, our win rate on winnable disputes went from something like 20 percent to over 60, not because the software was brilliant, but because it stopped good disputes from silently expiring.

Anselm Fowel, CTO and fintech architect
Anselm Fowel — CTO & fintech architect

Conclusion

If I could give one piece of advice to a team building payments from scratch, it would be to write the dispute model on day one, alongside the payment model, and never as an afterthought. The teams that treat chargebacks as a rare nuisance are the ones who end up doing an emergency ledger migration during their busiest quarter, usually right after their dispute ratio has drawn unwanted attention from their acquirer. Rita, for the record, still works with us. She runs the disputes team now, and the line she gives every new hire is the truest thing I know about this whole subject: the money you save is the money you never let quietly expire in an inbox. Get that one habit right and the reason codes, the evidence templates, the expected-value math all become detail. Miss it, and none of the clever parts will save you.

Enjoyed this article? Share it with others!

Share:

Get new posts in your inbox

Occasional, practical notes on engineering leadership, fintech, and building with AI. No spam, unsubscribe anytime.

Comments (0)

Leave a Comment

Comments are moderated and will appear after review.

No comments yet. Be the first to comment!

About the author

Anselm Fowel

Anselm Fowel

Chief Technology Officer & fintech architect. 16+ years leading engineering across AlliancePay, Mondu, Transalliance, Global Accelerex, and Fidelity Bank — writing here about engineering leadership, fintech architecture, and AI in production.

Read next

Subscribe to the newsletter

Practical notes on engineering leadership, fintech, and building with AI — delivered to your inbox. No spam, unsubscribe anytime.

Anselm Fowel

Chief Technology Officer | Fintech Architect | Engineering Leader

Building the future of financial technology through innovative engineering and strategic leadership.

Expertise

  • CTO Advisory
  • Fintech Architecture
  • Team Leadership
  • Technical Strategy
  • System Design

Get In Touch

[email protected]
Lagos, Nigeria

© 2026 Anselm Fowel. Crafted with passion.