Payments & Money APIs · Ledgers, Reconciliation & Risk

Refunds, Disputes & Chargebacks

Full and partial refunds, dispute evidence and their accounting.

Create a free account to save this page, take notes and track your progress. Sign in

Refunds are merchant-initiated: full or partial, idempotent, limited to the captured amount minus prior refunds, and asynchronous (they can fail, e.g. closed cards). Disputes/chargebacks are customer-initiated via the issuer: funds are withdrawn immediately plus a fee, and you can submit evidence before a deadline. Track both in your state machine and ledger, link them to the original payment, prevent refund + dispute double payouts to the customer, and measure dispute rates — networks penalize merchants above thresholds (monitoring programs around ~0.9–1% of transactions).

Refunds vs disputes

Criterion Refund Dispute / chargeback
Initiated by Merchant Cardholder via issuer
Funds movement Merchant → customer Withdrawn from merchant immediately
Fees Usually original fee not returned Dispute fee (often non-refundable)
Merchant response — Submit evidence by deadline
Risk signal Customer service Fraud/quality; network monitoring thresholds
Prevent duplicate compensation Check for open disputes Don't refund disputed charges (lose twice)

Python

async def refund(order_id: str, amount_minor: int, reason: str, requested_by: str):
    async with db.transaction():
        payment = await payments.for_update(order_id)
        if payment.disputed:
            raise Conflict("charge is disputed; respond to the dispute instead")
        if amount_minor > payment.captured_minor - payment.refunded_minor:
            raise UnprocessableEntity("refund exceeds refundable amount")
        refund_no = await payments.next_refund_number(payment.id)
    r = stripe.Refund.create(payment_intent=payment.psp_id, amount=amount_minor,
                             metadata={"order_id": order_id, "requested_by": requested_by},
                             idempotency_key=f"refund:{order_id}:{refund_no}")
    await payments.record_refund(payment.id, r.id, amount_minor, status=r.status)  # final status via webhook

Common pitfalls

  • Refunding a disputed charge (customer compensated twice).
  • Refund endpoints without authorization/approval limits.
  • Missing dispute deadlines.

Key points

  • Refunds are idempotent and bounded
  • Disputes: evidence and deadlines
  • Monitor dispute rates

Further reading: Stripe: Disputes

Practice questions

Why shouldn't you refund a charge that's already disputed?

Answer

The dispute already returns funds to the customer; refunding too means paying twice, and it usually doesn't close the dispute.

What limits should a refund API enforce?

Answer

Amount ≤ captured minus prior refunds, authorization/approval thresholds for staff, idempotency per refund request, and checks for open disputes.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?