Payment Orchestrator
Refund to Wallet

Refund to Wallet

Refund to Wallet credits a captured payment back into the buyer's in-program wallet balance — store credit the customer spends inside your ecosystem — instead of reversing it to the card network. It is a separate endpoint with its own permission; your existing card refund (POST /payments/:id/refund) is unchanged. Both are money-critical: this endpoint uses the same Step-Up (Bearer plus Ed25519 signature) and runs under one durable idempotency key.

All amounts are minor-unit integers (kuruş): "amount": 500 credits ₺5,00 to the wallet. Never send decimals or floats.

⚠️

This endpoint is POST /payments/:id/refund-to-wallet — not the card-refund endpoint. Use card refund to send money back to the card. Marketplace / split payments cannot be refunded to a wallet and must use card refund.

How it works

Call the refund-to-wallet endpoint

POST /payments/:id/refund-to-wallet with your Step-Up credentials and an Idempotency-Key. Include amount for a partial credit; omit it to credit everything still refundable. Include programId only if you run more than one wallet program.

The platform resolves the customer server-side

The buyer's phone and external id are read from the payment's recorded buyer — never from the request. The customer is deduped by phone within the program; if none exists, a wallet is auto-provisioned. Marketplace/split payments are rejected here.

The credit is applied to the wallet

The wallet is credited and the ledger is posted first, then the destination=WALLET refund line and the payment's state transition are finalized. The whole operation is idempotent on the durable refundId, so retries never double-credit.

The webhook confirms

wallet.refund.credited 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. The permission is payment:refund-to-wallet, distinct from the card-refund permission.

FieldTypeMeaning
amountinteger (minor units), optionalAmount to credit to the wallet. Omit to credit the full remaining refundable balance.
reasonstring, optionalFree-text reason, stored on the refund for your reporting.
programIdstring, optionalTarget wallet program. Omit when you run a single program (the wallet resolves it). Required only when you run multiple programs.

The buyer is not taken from the request. buyerPhone and externalCustomerId are resolved on the server from the payment's recorded buyer, so the DTO has no buyer field. A distinct target customer cannot be forced from the client.

# Partial credit of ₺5,00 to the buyer's wallet
curl https://api-gateway.lunixi.com/api/v1/payments/pay_01J9ZK3W8Q/refund-to-wallet \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Idempotency-Key: wallet-refund-84721-1" \
  -H "Content-Type: application/json" \
  # ...plus the Ed25519 request-signature headers — see Authentication
  -d '{
    "amount": 500,
    "reason": "Store credit"
  }'
 
# Full remaining balance to wallet — omit amount
curl https://api-gateway.lunixi.com/api/v1/payments/pay_01J9ZK3W8Q/refund-to-wallet \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Idempotency-Key: wallet-refund-84721-final" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Order cancelled" }'

Use a distinct Idempotency-Key per refund intent. The key becomes the durable refundId; retrying it safely replays the same wallet credit (network timeouts and reconcile sweeps won't double-credit), while a new key on the same payment issues an additional credit against the remaining balance.

Response

200 OK means the money was credited to the wallet or, when the buyer has no wallet, it was refunded to the card instead (see the fallback rules below). Reconcile from the webhook to know which path was taken.

Behavior

Program resolution — your own wallet program

programId is optional. Omit it and the wallet resolves the merchant's single program; if you run multiple programs, programId is required.

Phone dedup and auto-provision

For the program, end-user and wallet/account model, see Wallet — Core Concepts.

The wallet user is deduped by phone (primary) within the program: if a customer with that phone hash exists, the credit is written to them; otherwise a wallet is auto-provisioned from the phone plus externalCustomerId. Concurrent provisioning races resolve against the phone/external uniqueness constraint.

No-wallet fallback to card refund

If the buyer has no wallet and cannot be provisioned — no phone on the payment (NO_PHONE) or CANNOT_PROVISION — the flow automatically falls back to the standard card refund. The customer is still refunded and you still get 200.

Feature-disabled is an explicit error

When the feature is off, the wallet returns an inert DISABLED and the endpoint responds with an explicit error — it does not silently fall back to the card. That deliberate distinction separates "the buyer has no wallet" (fallback) from "the feature is turned off" (error).

Which statuses can be refunded to wallet

Payment statusRefundable to wallet?
SETTLEDYes — the supported status in v1
AUTHORIZEDNo — no captured money to credit yet
INITIATED, PROCESSING, AWAITING_3DNo — no captured money yet
REFUNDEDNo — nothing left to refund
Marketplace / split payment (any status)No — use card refund

The refundable ceiling shares the same math as card refund: cumulative captured_amount minus prior refunds.

Errors

SituationWhat you seeWhat to do
Marketplace / split paymentRejectedUse the card refund endpoint instead
Buyer has no phone / cannot be provisioned200 — refunded to the card insteadNone — the customer is refunded; reconcile from the webhook
Feature disabledExplicit DISABLED errorContact your account manager to enable Refund to Wallet
amount exceeds the remaining refundable balance422Read the payment, credit at most the remaining balance
Unknown payment id404Verify the paymentId
Wallet credit could not complete, or its outcome is not final yet502 payment.refund_to_wallet.credit_failedDo not start a card refund — the credit may have happened. Retry with the same Idempotency-Key or wait for the wallet.refund.credited webhook; the same key never creates a double credit
Duplicate request, same Idempotency-KeySame wallet credit replayedSafe — no double credit

Webhooks

EventFired when
wallet.refund.creditedThe refund was successfully credited to the wallet
wallet.refund.reversedAn operator reversed this wallet refund (the paired inverse of wallet.refund.credited)

Configure endpoints and delivery semantics in Webhooks.

Next steps

  • Refunds — returning captured money to the card, and the fallback path when the buyer has no wallet
  • Webhooks — receiving wallet.refund.credited and wallet.refund.reversed
  • Listing Payments — reading a payment's current status and history