Payments & Money APIs · Ledgers, Reconciliation & Risk

Refunds, Disputes & Chargebacks

Full and partial refunds, dispute evidence and their accounting.

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.