Back to BlogArchitecture & Engineering

Building a chargeback and dispute management workflow

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

RAITHub ships and tests production software. See QA as a Service or talk to us.

A chargeback is a deadline with money attached, so the system around it is a workflow, not a form. It must ingest the dispute, pull the money and fee out of your ledger, assemble evidence before a hard deadline, track the lifecycle as a state machine, and record the outcome correctly. Build it as a work queue with owners, because a missed deadline is a guaranteed loss.

If you would rather have this built for you, see how RAITHub would build it at the end of this guide.

What is the chargeback lifecycle you are modelling?

A chargeback (the card-network term is a dispute) happens when a cardholder asks their bank to reverse a charge. The bank pulls the money back immediately, usually with a fee, and gives you a window to respond with evidence. Win and the money returns; lose, or miss the deadline, and it is gone. Your system models that lifecycle and never lets a deadline pass unattended.

StageWhat happensWhat your system must do
Dispute openedBank reverses the charge and notifies the gatewayIngest the webhook; create a dispute record with its deadline
Funds withdrawnThe amount, plus a dispute fee, leaves your balancePost the reversal and fee to the ledger as separate entries
Evidence windowYou have a fixed number of days to respondAssemble evidence; surface the deadline; block it from being missed
SubmittedEvidence sent to the gatewayFreeze the case; record what was sent and when
ResolvedYou win (funds return) or lose (funds stay gone)Post the outcome to the ledger; close the case

Stripe documents the dispute fee it charges; its pricing page lists $15.00 per dispute received at the standard US price. Treat that fee, the reversed amount, and any returned funds as three distinct ledger movements so the books stay exact whichever way the case goes.

How should the dispute workflow be modelled in code?

As an explicit state machine with legal transitions, driven by gateway webhooks and user actions. Modelling states implicitly with booleans is how disputes get submitted twice or missed entirely.

type DisputeState =
  | 'needs_response'   // opened; evidence window running
  | 'under_review'     // evidence submitted; waiting on the network
  | 'won'
  | 'lost'
  | 'accepted'         // you chose not to contest

const TRANSITIONS: Record<DisputeState, DisputeState[]> = {
  needs_response: ['under_review', 'accepted'],
  under_review: ['won', 'lost'],
  won: [],
  lost: [],
  accepted: ['lost'], // accepting means you concede the funds
}

function transition(current: DisputeState, next: DisputeState): DisputeState {
  if (!TRANSITIONS[current].includes(next)) {
    throw new Error('illegal dispute transition: ' + current + ' -> ' + next)
  }
  return next
}

Gateway events move the case along; a webhook saying the network found in your favour transitions under_review to won. Each webhook handler must be idempotent, because gateways redeliver events, and a replayed "dispute resolved" must not post the outcome to your ledger twice. The pattern is in idempotency in API design, and webhook delivery is tested in testing payments and webhooks.

How do you make sure a deadline is never missed?

By treating every open dispute as a work item with an owner and an age, not a notification someone might read. The deadline is the whole point of the feature.

  • Store the response deadline on the record the moment the dispute opens, from the gateway's data.
  • Assign an owner. Disputes go to whoever handles them, the way a reconciliation exception goes to finance.
  • Escalate as the deadline nears, with reminders at set intervals, not a single easy-to-miss email.
  • Show a dashboard sorted by deadline, so the most urgent case is always first.
  • Record "accepted" as a deliberate choice, not an accident. Sometimes conceding a small, unwinnable dispute is the right call, but it should be a decision, not a lapse.

What evidence does a dispute response need?

Whatever proves the charge was legitimate and the customer received what they paid for. The system should assemble it automatically from data you already hold, so a human reviews and submits rather than hunts.

Dispute reasonEvidence to gather automatically
Product not receivedShipping and delivery confirmation, tracking, timestamps
Product unacceptableOrder details, product description, returns and refund policy, any support messages
Unrecognised chargeCustomer details, IP and device at checkout, billing descriptor, prior purchase history
DuplicateProof the two charges were separate orders, or evidence the duplicate was already refunded
Subscription cancelledCancellation date, terms accepted, usage after the disputed date

If a dispute's reason is "duplicate" and it turns out to be a real double charge, the right move is to accept and make sure it cannot recur, covered in stopping duplicate payments.

Buy, build or hire?

Most teams should use their gateway's built-in dispute tooling first, and build a workflow only when volume, custom evidence or several rails justify it.

OptionExampleChoose this whenWhere it stops
Gateway dispute dashboardYour payment provider's built-in dispute toolsLow dispute volume on one gatewayNo internal ownership, SLAs or ledger integration
Dispute automation SaaSThird-party chargeback toolsHigh volume and you want evidence automation out of the boxMonthly cost; limited to supported gateways
No-code workflowA task tool fed by a dispute webhookYou just need ownership and remindersNo evidence assembly or ledger accuracy
Custom buildYour own state machine, queue and ledger integrationSeveral rails, custom evidence, or disputes tied to your own dataYou own the maintenance as reasons and deadlines change

How long does it take to build yourself?

Our estimate, for a developer who knows the gateway's dispute API: two to three days to ingest dispute webhooks into a record with a deadline and a simple queue; two to four weeks to add the state machine, automatic evidence assembly, reminders and ledger integration with tests. The main risk of doing it alone is a webhook your handler drops or double-processes, which either misses a deadline or corrupts the ledger.

How RAITHub would build this

  • Ingest: idempotent webhook handlers that create a dispute record with its deadline and post the reversal and fee to the ledger.
  • Workflow: an explicit state machine, an owner per case, and deadline-sorted reminders that escalate.
  • Evidence: automatic assembly from your order, shipping and customer data for a human to review and submit.
  • Outcome: the win, loss or accept posted correctly to the ledger, with a full history.

Timeline: 4–6 weeks as a SaaS feature on a fixed scope, shorter if you only need ingestion, ownership and reminders. What you receive: automated tests and CI built on replayable webhook fixtures, handover docs, and full IP under NDA. Production and financial data stay in your own cloud account; development uses synthetic data.

RAITHub is an engineering studio and has not shipped a regulated or licensed fintech product; how you account for disputes and fees is a question for your accountant, and this is general information, so confirm with your adviser. Proof we can point to: TheSkinProof, the founder's own venture, runs multi-gateway payments with 750+ automated tests, and PadhAI, built by RAITHub, was designed for 9 gateways behind one abstraction. Next step: a free 15-minute technical audit, then a written fixed quote. Book the audit. More is on the SaaS development page, the FinTech page, and in our payments engineering guide.

Frequently asked questions

What is a chargeback?

A dispute where a cardholder asks their bank to reverse a charge. The bank pulls the money back immediately, usually with a fee, and gives you a window to respond with evidence. Win and the funds return; lose or miss the deadline and they are gone.

How much does a chargeback cost?

You lose the disputed amount unless you win, plus a dispute fee. Stripe lists $15.00 per dispute received at its standard US price. Account for the reversed amount, the fee, and any returned funds as separate ledger movements.

Why model disputes as a state machine?

Because a dispute has legal and illegal transitions, and modelling states with scattered booleans leads to cases submitted twice or missed. An explicit state machine with allowed transitions makes the invalid moves impossible.

How do I never miss a dispute deadline?

Store the deadline on the record when the dispute opens, assign an owner, escalate with reminders as it nears, and show a dashboard sorted by deadline. Treat every open dispute as a work item, not a notification.

Should I build this or use my gateway's tools?

Start with the gateway's dispute dashboard if volume is low and you are on one gateway. Build a custom workflow when you need internal ownership and SLAs, automatic evidence assembly, several rails, or tight ledger integration.

chargeback managementdispute workflowFinTechstate machinewebhooksledger

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.