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.