Payment Orchestrator
Pay with Wallet

Pay with Wallet — Payment

POST /payments/direct/payment with paymentMethod: "WALLET" takes the payment from the buyer's wallet. The buyer approves the payment with a one-time code sent to their phone; you send the code with confirm. This page shows every request field, the response, the confirmation and the errors for wallet payments; the action, confirm and error-code rules shared by all methods are on Payment.

The wallets available on your account are in the providers list of the WALLET row of the GET /payments/methods response. If your business's own wallet program is connected, it is in this list too, with your program's name and logo.

How it works

Choose the wallet

When one wallet is connected, wallet.providerCode does not need to be sent. When more than one is connected, send the code of the wallet the buyer chose; otherwise you get 400 payment.provider.required. See Choosing the provider.

Start the payment

In the wallet block send the phone number of the buyer's wallet account (gsmNumber) and any other fields in the wallet's inputFields definition.

Receive the code

The response (202) carries an OTP action. The code is sent to the action.otp.maskedTarget number. When the action carries charge, show the buyer the total before they confirm.

Confirm

Send the code the buyer entered with POST /payments/{paymentId}/actions/{actionId}/confirm. The result returns with 200 and payment.captured is sent.

Choosing the provider

With a wallet the buyer chooses the provider: on the WALLET row providerSelectionMode is BUYER. The platform does not choose a wallet on the buyer's behalf.

  • When one wallet is connected, wallet.providerCode does not need to be sent; that wallet is used. On the method row providerSelection is OPTIONAL.
  • When more than one wallet is connected, show the buyer the providers[] list of the WALLET row (displayName, logoUrl) and send the providerCode of the wallet they choose in wallet.providerCode. On the method row providerSelection is REQUIRED. Without the code the request gets 400 payment.provider.required; the connected codes are in details.boundProviderCodes.
  • If the code you send is not connected to this method on your account, the request gets 422 payment.provider.not_available.
  • The chosen wallet is pinned to the payment: on an error the payment does not move to another wallet, another method or card.

Selection modes and how they relate to providerSelection: Listing Methods › Provider selection.

Request

POST https://api-gateway.lunixi.com/api/v1/payments/direct/payment
Content-Type: application/json

The request carries Step-Up authentication (Bearer token and Ed25519 signature headers; see Authentication) and a required Idempotency-Key header (at most 200 characters); the rules are the same as in Payment — Idempotency.

Method fields

FieldTypeRequiredDescription
paymentMethodstringYesWALLET
walletobjectYesWallet block; its fields are in the rows below
wallet.providerCodestringWhen more than one wallet is connectedProvider code of the wallet the buyer chose (GET /payments/methods); see Choosing the provider
wallet.gsmNumberstringWhen the wallet's definition asks for itPhone of the wallet account; the verification code goes to this number. When omitted, buyer.gsmNumber is used
wallet.<field>—Per definitionOther fields in the wallet's inputFields definition; see Method fields. Fields that are not in the definition cannot be sent
preauthbooleanNotrue: pre-authorization; the selected wallet must support it. See Pre-authorization
callbackUrlstringNohttp or https, at most 2048 characters. If the provider returns a REDIRECT action, the buyer's browser returns here with the result

The body contains only the wallet block: another method's block, or card, force3D, installment, installmentQuoteToken, gets 400 VALIDATION_ERROR.

Order fields

FieldTypeRequiredDescription
amountintegerYesAmount, minor-unit integer; at least 1. It is the basket total: the price of the goods and services the buyer pays for, excluding the installment fee. If you send basketItems, it must equal the sum of the items' price. For card installments the system adds the fee. Example: 10050
currencystring (enum)YesISO 4217 code: TRY, USD, EUR, GBP, RUB, AED, SAR, QAR, KWD, JPY, CHF, CAD, AUD, CNY, AZN. A currency not enabled on your account is rejected
orderIdstringYesYour order number, at most 128 characters. Example: ORD-84722
customerIdstringNoYour customer key. It identifies the buyer for wallet payments; when a card is saved with 3D Secure, it is stored under this key. Example: cust_7788
descriptionstringNoPayment description
localestringNoMessage language, default tr
paymentChannelstringNoPayment channel. Example: WEB
paymentGroupstringNoProduct group. Example: PRODUCT
buyerobjectNoBuyer details; see buyer
shippingAddressobjectNoShipping address; see Address
billingAddressobjectNoBilling address; see Address
basketItemsarrayNoBasket items; see basketItems[]
metadataobjectNoFree key-value object for your own records. Request a pre-authorization with the preauth field, not with metadata

The amount is sent only in amount; the removed amount fields get 400 payment.amount.field_removed. Sending a field that is not defined here rejects the request with 400 VALIDATION_ERROR.

For wallet payments customerId identifies the buyer and is recommended for your own wallet program; see Buyer identity. buyer.gsmNumber is used when wallet.gsmNumber is omitted.

buyer

buyer is optional. If you send it, every field marked "Yes" below must be filled. On a Checkout Form intent (POST /payments/intents, POST /payments/marketplace/intents) this rule does not apply: every field is optional, and the fields you send are checked for type and length.

FieldTypeRequiredExample
idstringNoCUST-1
namestringYesAda
surnamestringYesYılmaz
identityNumberstringYes11111111111
emailstring (email)Yes[email protected]
gsmNumberstringYes+905000000000
citystringYesIstanbul
countrystringYesTurkey
zipCodestringYes34732
ipstringYesThe buyer's IP address
registrationDatestringNo2024-04-21 15:12:09
lastLoginDatestringNo2026-10-05 12:43:35
registrationAddressstringNoRegistered address

Required-field rule, how it differs from customerId, and error field paths: Buyer.

Address

shippingAddress and billingAddress share the same shape. If you send the object, all five fields are required.

FieldTypeExample
contactNamestringAda Yılmaz
addressstringAltunizade Mah. …
citystringIstanbul
countrystringTurkey
zipCodestring34742

How the two addresses differ and error field paths: Addresses.

basketItems[]

FieldTypeRequiredDescription
idstringYesItem code
namestringYesItem name
category1stringYesMain category
category2stringNoSub-category
itemTypestring (enum)YesPHYSICAL or VIRTUAL
priceintegerYesItem amount, minor-unit integer, at least 0

The sum of the items' price must equal amount; otherwise the request is rejected with 400 VALIDATION_ERROR. The installment fee is not added to the items.

Example request

# See the Authentication page for how the Step-Up signature headers are computed.
curl -X POST https://api-gateway.lunixi.com/api/v1/payments/direct/payment \
  -H "Authorization: Bearer at_test_5f3e2b1a..." \
  -H "Content-Type: application/json" \
  -H "X-Key-Id: mk_test_sk_..." \
  -H "X-Date: 2026-10-11T09:00:00.000Z" \
  -H "X-Nonce: 4a7e1c92-3b5d-4f60-8e2a-9c1d7b3e5f08" \
  -H "Digest: SHA-256=..." \
  -H "X-Signature: ..." \
  -H "Idempotency-Key: ORDER-1004-1" \
  -d '{
    "paymentMethod": "WALLET",
    "orderId": "ORDER-1004",
    "amount": 12500,
    "currency": "TRY",
    "customerId": "CUST-2",
    "wallet": { "providerCode": "<providerCode>", "gsmNumber": "+905321234567" }
  }'

<providerCode> is a placeholder; read the wallet's code from the GET /payments/methods response.

Buyer identity

For payments with your business's own wallet program, the wallet is found with two pieces of data:

  • customerId: Matches the buyer to the customer record in your wallet program. When you send it, the phone is only used for verification; if the phone does not match that customer's registered number the request is rejected.
  • wallet.gsmNumber: A phone number is always required. The verification code goes to this number; the code proves that the person paying owns the wallet. When the field is omitted, buyer.gsmNumber is used. If neither is present or the number is malformed the request gets 400 payment.method.block_invalid with details.field set to wallet.gsmNumber.

The buyer's identity is read only from the customerId field. If metadata.customerId is sent on its own or differs from customerId, the request gets 400 payment.method.customer_identity_conflict.

Response and action

{
  "status": "partial",
  "code": "CUSTOMER_ACTION_REQUIRED",
  "message": "…",
  "data": {
    "paymentId": "3a8d5f1c-6b2e-4c9a-8e7d-1f0b2c3d4e5f",
    "orderId": "ORDER-1004",
    "status": "INITIATED",
    "paymentMethod": "WALLET",
    "providerCode": "<providerCode>",
    "action": {
      "id": "act_8e1f2a3b4c5d6e7f8091a2b3c4d5e6f7",
      "type": "OTP",
      "expiresAt": "2026-10-11T09:03:00.000Z",
      "otp": { "maskedTarget": "53****67" },
      "charge": {
        "amount": 12500,
        "feeTotal": 250,
        "taxTotal": 50,
        "buyerTotalCharge": 12800,
        "currency": "TRY"
      },
      "maskedBusinessName": "Ex***** St***"
    }
  },
  "statusCode": 202,
  "requestId": "…"
}
FieldDescription
action.otp.maskedTargetMasked number the code was sent to. When there is no otp field no code is required; send confirm without otp
action.chargeTotal the buyer pays and its breakdown, minor units. When fees or tax are passed on to the buyer, buyerTotalCharge is greater than the order amount (amount). Show the buyer buyerTotalCharge before they confirm
action.maskedBusinessNameMasked name of the business receiving the payment
action.stepUpRequiredPresent only when true: the wallet requested strong verification. No extra input is expected from you

For payments with your business's own wallet program, the payment stays INITIATED until the code is confirmed.

Confirm

{ "otp": "123456" }

Send it with POST /payments/{paymentId}/actions/{actionId}/confirm; authentication and the outcome table are the same as in Payment — confirm.

  • A wrong code gets 422 payment.action.input_rejected; the action stays open and the remaining attempts are in details.attemptsRemaining. Your own wallet program allows 3 attempts.
  • Your own wallet program cannot resend the code: resend gets 422 payment.action.resend_not_supported. If the code expires, start a new payment.

Result

SituationResponseWhat to do
Payment completed200, data.status CAPTUREDShip the order on payment.captured
The wallet declined the payment (balance, limit, verification …)400 or 402, code payment.wallet_payment.*No money moved. Offer the buyer another method
The approval was sent but the result is not final yet409 payment.wallet_payment.not_settledDo not treat the order as paid; repeat the same confirm request shortly. Once the payment settles the same confirm returns the result and payment.captured is sent

For payments with your business's own wallet program:

  • In a decline data.status may stay INITIATED; no money moved and payment.failed is not sent. To retry, start a new payment with a new Idempotency-Key. A payment record that is not completed becomes EXPIRED after 24 hours.
  • If the action expires before an approval is sent, the action closes and the payment stays INITIATED.

Common wallet codes:

CodeMeaning
payment.wallet_payment.insufficient_balanceThe wallet balance is not enough
payment.wallet_payment.limit_exceededThe wallet's spending limit is not enough for this payment
payment.wallet_payment.kyc_insufficientThe wallet's identity verification level is not enough for this payment
payment.wallet_payment.customer_not_matchedNo wallet matches customerId
payment.wallet_payment.identity_mismatchThe phone does not match the number registered on this customer's wallet
payment.wallet_payment.provider_unavailableThe selected wallet is not available on your account
payment.wallet_payment.rejectedThe wallet did not complete the payment; no details are shared
payment.wallet_payment.not_settledThe approval was sent; the result is not final yet

Pre-authorization

Whether a wallet supports pre-authorization is shown by the preauth field of the GET /payments/methods response. With a wallet that does not, preauth: true gets 422 payment.preauth.unsupported_for_method before the payment starts. Your business's own wallet program does not support pre-authorization.

Errors

SituationResponse
More than one wallet connected, no providerCode400 payment.provider.required
The selected wallet is not connected422 payment.provider.not_available
Phone missing or malformed400 payment.method.block_invalid
metadata.customerId conflicts with customerId400 payment.method.customer_identity_conflict
The selected wallet's verification flow is not supported on this endpoint422 payment.method.wallet_bridge_not_available
Pre-authorization not supported422 payment.preauth.unsupported_for_method

Other codes: Payment — Error codes.

Webhooks

EventWhen
payment.capturedThe wallet payment completed. Ship the order on this event
payment.authorizedThe amount was held for a pre-authorization (AUTHORIZED); no funds were captured yet. See Pre-authorization
payment.failedThe payment failed (with your own wallet program a decline does not make the payment FAILED)

The data of the events carries paymentMethod: "WALLET" and the wallet's providerCode. See Webhooks.

Next steps