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.providerCodedoes not need to be sent; that wallet is used. On the method rowproviderSelectionisOPTIONAL. - When more than one wallet is connected, show the buyer the
providers[]list of theWALLETrow (displayName,logoUrl) and send theproviderCodeof the wallet they choose inwallet.providerCode. On the method rowproviderSelectionisREQUIRED. Without the code the request gets400payment.provider.required; the connected codes are indetails.boundProviderCodes. - If the code you send is not connected to this method on your account, the request gets
422payment.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/jsonThe 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
| Field | Type | Required | Description |
|---|---|---|---|
paymentMethod | string | Yes | WALLET |
wallet | object | Yes | Wallet block; its fields are in the rows below |
wallet.providerCode | string | When more than one wallet is connected | Provider code of the wallet the buyer chose (GET /payments/methods); see Choosing the provider |
wallet.gsmNumber | string | When the wallet's definition asks for it | Phone of the wallet account; the verification code goes to this number. When omitted, buyer.gsmNumber is used |
wallet.<field> | — | Per definition | Other fields in the wallet's inputFields definition; see Method fields. Fields that are not in the definition cannot be sent |
preauth | boolean | No | true: pre-authorization; the selected wallet must support it. See Pre-authorization |
callbackUrl | string | No | http 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
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | Yes | Amount, 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 |
currency | string (enum) | Yes | ISO 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 |
orderId | string | Yes | Your order number, at most 128 characters. Example: ORD-84722 |
customerId | string | No | Your 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 |
description | string | No | Payment description |
locale | string | No | Message language, default tr |
paymentChannel | string | No | Payment channel. Example: WEB |
paymentGroup | string | No | Product group. Example: PRODUCT |
buyer | object | No | Buyer details; see buyer |
shippingAddress | object | No | Shipping address; see Address |
billingAddress | object | No | Billing address; see Address |
basketItems | array | No | Basket items; see basketItems[] |
metadata | object | No | Free 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.
| Field | Type | Required | Example |
|---|---|---|---|
id | string | No | CUST-1 |
name | string | Yes | Ada |
surname | string | Yes | Yılmaz |
identityNumber | string | Yes | 11111111111 |
email | string (email) | Yes | [email protected] |
gsmNumber | string | Yes | +905000000000 |
city | string | Yes | Istanbul |
country | string | Yes | Turkey |
zipCode | string | Yes | 34732 |
ip | string | Yes | The buyer's IP address |
registrationDate | string | No | 2024-04-21 15:12:09 |
lastLoginDate | string | No | 2026-10-05 12:43:35 |
registrationAddress | string | No | Registered 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.
| Field | Type | Example |
|---|---|---|
contactName | string | Ada Yılmaz |
address | string | Altunizade Mah. … |
city | string | Istanbul |
country | string | Turkey |
zipCode | string | 34742 |
How the two addresses differ and error field paths: Addresses.
basketItems[]
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Item code |
name | string | Yes | Item name |
category1 | string | Yes | Main category |
category2 | string | No | Sub-category |
itemType | string (enum) | Yes | PHYSICAL or VIRTUAL |
price | integer | Yes | Item 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.gsmNumberis used. If neither is present or the number is malformed the request gets400payment.method.block_invalidwithdetails.fieldset towallet.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": "…"
}| Field | Description |
|---|---|
action.otp.maskedTarget | Masked number the code was sent to. When there is no otp field no code is required; send confirm without otp |
action.charge | Total 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.maskedBusinessName | Masked name of the business receiving the payment |
action.stepUpRequired | Present 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
422payment.action.input_rejected; the action stays open and the remaining attempts are indetails.attemptsRemaining. Your own wallet program allows 3 attempts. - Your own wallet program cannot resend the code: resend gets
422payment.action.resend_not_supported. If the code expires, start a new payment.
Result
| Situation | Response | What to do |
|---|---|---|
| Payment completed | 200, data.status CAPTURED | Ship 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 yet | 409 payment.wallet_payment.not_settled | Do 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.statusmay stayINITIATED; no money moved andpayment.failedis not sent. To retry, start a new payment with a newIdempotency-Key. A payment record that is not completed becomesEXPIREDafter 24 hours. - If the action expires before an approval is sent, the action closes and the payment stays
INITIATED.
Common wallet codes:
| Code | Meaning |
|---|---|
payment.wallet_payment.insufficient_balance | The wallet balance is not enough |
payment.wallet_payment.limit_exceeded | The wallet's spending limit is not enough for this payment |
payment.wallet_payment.kyc_insufficient | The wallet's identity verification level is not enough for this payment |
payment.wallet_payment.customer_not_matched | No wallet matches customerId |
payment.wallet_payment.identity_mismatch | The phone does not match the number registered on this customer's wallet |
payment.wallet_payment.provider_unavailable | The selected wallet is not available on your account |
payment.wallet_payment.rejected | The wallet did not complete the payment; no details are shared |
payment.wallet_payment.not_settled | The 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
| Situation | Response |
|---|---|
More than one wallet connected, no providerCode | 400 payment.provider.required |
| The selected wallet is not connected | 422 payment.provider.not_available |
| Phone missing or malformed | 400 payment.method.block_invalid |
metadata.customerId conflicts with customerId | 400 payment.method.customer_identity_conflict |
| The selected wallet's verification flow is not supported on this endpoint | 422 payment.method.wallet_bridge_not_available |
| Pre-authorization not supported | 422 payment.preauth.unsupported_for_method |
Other codes: Payment — Error codes.
Webhooks
| Event | When |
|---|---|
payment.captured | The wallet payment completed. Ship the order on this event |
payment.authorized | The amount was held for a pre-authorization (AUTHORIZED); no funds were captured yet. See Pre-authorization |
payment.failed | The 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
- Listing Methods — connected wallets and provider selection
- Payment — common request, response and error codes
- Checkout Form — Payment — the wallet flow in the form