Refunds
A refund returns captured money to the buyer's card. One call handles both cases: omit the amount for a full refund, or pass an amount for a partial refund — and issue as many partials as you need until the payment is fully refunded. The platform tracks the cumulative refunded total per payment and enforces that you can never refund more than was charged.
All amounts are minor-unit integers (kuruş): "amount": 2500 refunds ₺25,00. Never send decimals or floats.
Refund is for captured money. If the payment is still only AUTHORIZED (funds reserved, not captured), release it with void instead — that lifts the hold without a money movement back to the card.
How it works
Call the refund endpoint
POST /payments/:id/refund with your Step-Up credentials and an Idempotency-Key. Include amount for a partial refund; omit it to refund everything still refundable.
The platform validates and executes
The payment's status and cumulative refund counter are checked — the request is rejected if the payment isn't refundable or the amount exceeds what's left. The refund is then submitted to the provider that processed the original charge.
The payment status updates
Partial refunds move the payment to PARTIALLY_REFUNDED; once the cumulative refunded total reaches the captured amount, it becomes REFUNDED.
The webhook confirms
payment.partially_refunded or payment.refunded is delivered to your webhook endpoint. Reconcile your order state from the webhook, not just the synchronous response.
Request
Authentication is Step-Up: your at_live_… Bearer token plus an Ed25519 request signature, required on every money-movement endpoint — see Authentication.
| Field | Type | Meaning |
|---|---|---|
amount | integer (minor units), optional | Amount to refund. Omit for a full refund of the remaining refundable balance. |
reason | string, optional | Free-text reason, stored on the refund for your reporting. |
# Partial refund of ₺25,00
curl https://api-gateway.lunixi.com/api/v1/payments/pay_01J9ZK3W8Q/refund \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-H "Idempotency-Key: refund-order-84721-1" \
-H "Content-Type: application/json" \
# ...plus the Ed25519 request-signature headers — see Authentication
-d '{
"amount": 2500,
"reason": "One item returned"
}'
# Full refund — omit amount
curl https://api-gateway.lunixi.com/api/v1/payments/pay_01J9ZK3W8Q/refund \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-H "Idempotency-Key: refund-order-84721-final" \
-H "Content-Type: application/json" \
-d '{ "reason": "Order cancelled" }'Use a distinct Idempotency-Key per refund intent. Retrying the same key safely replays the same refund (network timeouts won't double-refund); a new key on the same payment creates an additional partial refund.
Response
{
"paymentId": "pay_01J9ZK3W8Q",
"status": "PARTIALLY_REFUNDED",
"amount": 10050,
"currency": "TRY"
}The amount in the response is the payment's charged amount (installment fee included). When the cumulative refunded amount reaches the captured amount, status becomes REFUNDED.
Full vs partial — the cumulative counter
Every refund is applied against the payment's remaining refundable balance:
| Scenario | Result |
|---|---|
Refund with no amount | Refunds the entire remaining balance → REFUNDED |
Partial amount < remaining | Payment → PARTIALLY_REFUNDED; counter increases |
| Several partials that sum to the captured total | Last one flips the payment to REFUNDED |
amount > remaining balance | Rejected — over-refunding is impossible |
For a payment captured partially (PARTIALLY_CAPTURED), the refundable ceiling is the captured amount, not the originally authorized amount.
Which statuses can be refunded
| Payment status | Refundable? |
|---|---|
CAPTURED, SETTLED, PARTIALLY_CAPTURED | Yes |
PARTIALLY_REFUNDED | Yes — up to the remaining balance |
AUTHORIZED | No — use void to release the hold |
REFUNDED | No — nothing left to refund |
INITIATED, PROCESSING, AWAITING_3D | No — no captured money yet |
FAILED, VOIDED, EXPIRED | No — no money moved |
CHARGEDBACK | No — handle through the chargeback lifecycle |
Split payments. A refund on the payment record of a Checkout Form order paid with several cards or methods is distributed across the parts; amount is required for this refund and the per-part result is returned in meta.details.splitRefund. The distribution rule and the refund endpoint for split payments started from the API: Split Payments › Refunds.
Errors
| Situation | What you see | What to do |
|---|---|---|
| Payment not in a refundable status | 409 | Check GET /payments/:id; void instead if AUTHORIZED |
amount exceeds the remaining refundable balance | 422 | Read the payment, refund at most the remaining balance |
| Already fully refunded | 409 | Nothing to do — treat as success in reconciliation |
| Unknown payment id | 404 | Verify the paymentId |
Duplicate request, same Idempotency-Key | Same refund result replayed | Safe — no double refund |
| Provider-side refund failure | Error response; payment status unchanged | Retry later with the same Idempotency-Key |
Webhooks
| Event | Fired when |
|---|---|
payment.partially_refunded | A partial refund succeeded; balance remains |
payment.refunded | The payment is now fully refunded |
Configure endpoints and delivery semantics in Webhooks.
Next steps
- Pre-authorization, Capture & Void — releasing an uncaptured authorization
- Listing Payments — reading a payment's current status and history
- Chargebacks — when the buyer disputes instead of asking you for a refund