Refunds, disputes and fraud Required
Refund through the provider with a key and a reason
Issue every refund through the provider's API with an idempotency key, a reason and the person who asked. Track it until it succeeds or fails.
How to do it
Refund to the original payment method, unless the customer agrees to something else. Allow partial refunds only up to the amount not yet refunded, checked on the server inside a transaction. Refunds can fail, such as on a closed card, so handle the provider's refund failure event. Write a ledger row as in payment ledger. Issue a credit note for every refund of an invoiced charge.
Example
async function refund(order, amountMinor, reason, staffId) {
const remaining = order.paidMinor - order.refundedMinor
if (amountMinor <= 0 || amountMinor > remaining) throw new Error(`Refund must be between 1 and ${remaining}`)
return stripe.refunds.create(
{ payment_intent: order.paymentId, amount: amountMinor, metadata: { orderId: order.id, reason, staffId } },
{ idempotencyKey: `refund-${order.id}-${order.refundCount + 1}` },
)
}
def refund(order, amount_minor: int, reason: str, staff_id: str):
remaining = order.paid_minor - order.refunded_minor
if not 0 < amount_minor <= remaining:
raise ValueError(f"Refund must be between 1 and {remaining}")
return stripe.Refund.create(
payment_intent=order.payment_id,
amount=amount_minor,
metadata={"order_id": order.id, "reason": reason, "staff_id": staff_id},
idempotency_key=f"refund-{order.id}-{order.refund_count + 1}",
)
How to check
Send the same refund request twice at once. The provider shows one refund. A refund larger than the remaining amount is rejected before any provider call.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers payments playbook: https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/provider-refunds.