OI Payments Docs
Guides

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" }'
FieldRequiredNotes
amountMinoryesInteger minor units, > 0, ≤ the refundable balance.
reasonnoFree 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, or SUCCEEDED reserve balance.
  • A FAILED refund 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
StatusMeaningWebhook
AWAITING_APPROVALParked for operator sign-off (not pushed).
PENDINGAccepted by the gateway, settling.refund.pending
SUCCEEDEDSettled to the customer.refund.succeeded
FAILEDRejected; 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.

On this page