Payment Orchestrator
Refunds

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.

FieldTypeMeaning
amountinteger (minor units), optionalAmount to refund. Omit for a full refund of the remaining refundable balance.
reasonstring, optionalFree-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:

ScenarioResult
Refund with no amountRefunds the entire remaining balance → REFUNDED
Partial amount < remainingPayment → PARTIALLY_REFUNDED; counter increases
Several partials that sum to the captured totalLast one flips the payment to REFUNDED
amount > remaining balanceRejected — 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 statusRefundable?
CAPTURED, SETTLED, PARTIALLY_CAPTUREDYes
PARTIALLY_REFUNDEDYes — up to the remaining balance
AUTHORIZEDNo — use void to release the hold
REFUNDEDNo — nothing left to refund
INITIATED, PROCESSING, AWAITING_3DNo — no captured money yet
FAILED, VOIDED, EXPIREDNo — no money moved
CHARGEDBACKNo — 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

SituationWhat you seeWhat to do
Payment not in a refundable status409Check GET /payments/:id; void instead if AUTHORIZED
amount exceeds the remaining refundable balance422Read the payment, refund at most the remaining balance
Already fully refunded409Nothing to do — treat as success in reconciliation
Unknown payment id404Verify the paymentId
Duplicate request, same Idempotency-KeySame refund result replayedSafe — no double refund
Provider-side refund failureError response; payment status unchangedRetry later with the same Idempotency-Key

Webhooks

EventFired when
payment.partially_refundedA partial refund succeeded; balance remains
payment.refundedThe payment is now fully refunded

Configure endpoints and delivery semantics in Webhooks.

Next steps