Refunds
Issue full or partial refunds, understand the over-refund guard and approval threshold, and track async outcomes.
Refund a captured payment in full or in part. Refunds settle asynchronously through the gateway, so — like payments — you react to the confirmed state.
Create a refund
POST /payments/{id}/refunds against a SUCCEEDED payment:
curl -X POST http://localhost:8080/api/v1/payments/100/refunds \
-H "X-Api-Key: oi_test_xxx" -H "X-Api-Secret: your-secret" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rf-9b1c…" \
-d '{ "amountMinor": 2500, "reason": "Damaged item" }'| Field | Required | Notes |
|---|---|---|
amountMinor | yes | Integer minor units, > 0, ≤ the refundable balance. |
reason | no | Free text, up to 500 characters. |
The over-refund guard
You can issue several partial refunds, but their total can never exceed the captured amount. The refundable balance is the captured amount minus every refund still holding it:
- Refunds in
AWAITING_APPROVAL,PENDING, orSUCCEEDEDreserve balance. - A
FAILEDrefund frees its reservation, so the amount becomes refundable again.
This is what makes retrying a failed refund safe: the failed attempt consumed nothing, so re-issuing the same amount can never compound. The server is the final authority on the balance.
Approval threshold (operator refunds)
Refunds initiated by an operator in the dashboard above the app's configured
approval threshold are parked in AWAITING_APPROVAL and must be released by a
holder of the refund:approve permission before they go to the gateway. Refunds
created through the app API are not subject to the threshold — they go straight
to PENDING.
Lifecycle
stateDiagram-v2
[*] --> AWAITING_APPROVAL: operator, above threshold
[*] --> PENDING: accepted by gateway
AWAITING_APPROVAL --> PENDING: approved
PENDING --> SUCCEEDED: gateway confirmed
PENDING --> FAILED: gateway rejected| Status | Meaning | Webhook |
|---|---|---|
AWAITING_APPROVAL | Parked for operator sign-off (not pushed). | — |
PENDING | Accepted by the gateway, settling. | refund.pending |
SUCCEEDED | Settled to the customer. | refund.succeeded |
FAILED | Rejected; balance freed. | refund.failed |
When all captured value is refunded the parent payment becomes REFUNDED;
otherwise PARTIALLY_REFUNDED.
Track the outcome
Read a single refund with GET /refunds/{id}, or react to the
refund.pending / refund.succeeded / refund.failed
webhooks. A refund that is still AWAITING_APPROVAL emits no webhook until it is
accepted by the gateway.