Payment Orchestrator
Mobile Billing

Mobile Billing — Payment

POST /payments/direct/payment with paymentMethod: "MOBILE_BILLING" adds the amount to the buyer's mobile phone bill (Faturana Yansıt). The buyer enters the code sent to their line and accepts the provider's agreement; you send both with confirm. This page shows every request field, the response, the confirmation and the errors for mobile billing; the action, confirm and error-code rules shared by all methods are on Payment.

Whether the method is open on your account, its provider, amount limits and the fields asked from the buyer are in the MOBILE_BILLING row of the GET /payments/methods response.

How it works

Start the payment

Send the line to be billed in mobileBilling.gsmNumber. If you let the buyer choose the provider, send its code in mobileBilling.providerCode; if you omit it, the platform chooses the provider. See Choosing the provider.

Show the code and the agreement

The response (202) carries an OTP action: the code goes to the action.otp.maskedTarget line by SMS. When action.consent.required is true, show the buyer the agreement at action.consent.url and get their acceptance.

Confirm

Send the code and the consent with confirm: { "otp": "…", "consentAccepted": true }. When the payment completes the response is 200 and payment.captured is sent.

If the line was already approved for this method, the provider may complete the payment without a code: in that case there is no action in the response and the result returns directly.

Choosing the provider

On this endpoint you do not have to choose the mobile billing provider: on the MOBILE_BILLING row providerSelectionMode is PLATFORM and providerSelection is OPTIONAL. mobileBilling.providerCode is optional:

  • If you omit it, the platform picks, before any money moves, one of the providers connected to your account that can take this payment, and pins it to the payment. When one provider is connected, that one is used.
  • If you send it, that provider is used. If the code is not connected to this method on your account, the request gets 422 payment.provider.not_available.
  • After the choice the payment does not move to another provider, another method or card. The provider that runs the payment is in the providerCode field of the response and the webhook.
  • If none of the connected providers can take this payment, the request gets 422 payment.provider.none_eligible; see No provider is eligible.

The checks the platform uses to choose: Listing Methods › How the platform chooses.

You can show the providers on your own payment page in the order of the GET /payments/methods response (displayName, logoUrl) and send the code the buyer chooses. In the Checkout Form the buyer always chooses the provider → Checkout Form — Payment › Provider choice.

No provider is eligible

When more than one provider is connected, you omit providerCode and none of them can take this payment, the payment is not started, nothing is added to the bill and no code is sent:

{
  "status": "failure",
  "code": "payment.provider.none_eligible",
  "message": "None of the connected providers can take this payment; the payment was not started and nothing was charged. …",
  "statusCode": 422,
  "timestamp": "2026-10-12T09:00:00.000Z",
  "service": "api-gateway",
  "systemTime": 1791795600000,
  "requestId": "…",
  "docsUrl": "https://docs.lunixi.com/getting-started/errors#payment.provider.none_eligible",
  "details": {
    "paymentMethod": "MOBILE_BILLING",
    "candidates": [
      { "providerCode": "<providerCode>", "outcome": "INELIGIBLE", "reason": "AMOUNT_OUT_OF_RANGE" },
      { "providerCode": "<providerCode-2>", "outcome": "UNAVAILABLE", "reason": "ELIGIBILITY_TIMEOUT" }
    ]
  }
}

details.candidates[] carries one row per provider. The rows contain no personal data.

FieldDescription
providerCodeThe provider's code
outcomeINELIGIBLE: the provider cannot take this payment. UNAVAILABLE: no eligibility answer could be obtained from the provider. NOT_CHECKED: the provider was not tried because the selection time ran out
reasonReason code or null. Examples: CURRENCY_NOT_SUPPORTED, AMOUNT_OUT_OF_RANGE, PREAUTH_UNSUPPORTED, INPUT_INVALID, ELIGIBILITY_ERROR, ELIGIBILITY_TIMEOUT, SELECTION_BUDGET_EXHAUSTED; it can also be the provider's own decline code
  • If any result is UNAVAILABLE or NOT_CHECKED, you can retry the same request later.
  • You can also choose the provider yourself with mobileBilling.providerCode.
  • If every result is INELIGIBLE, offer the buyer another method.

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
paymentMethodstringYesMOBILE_BILLING
mobileBillingobjectYesMobile billing block; its fields are in the rows below
mobileBilling.gsmNumberstringYes*Line to be billed; optional + and 10–15 digits. The code is sent to this line. *When omitted, buyer.gsmNumber is used; if neither is present the request gets 400 payment.method.block_invalid
mobileBilling.providerCodestringNoProvider code (GET /payments/methods). When omitted, the platform chooses the provider; see Choosing the provider
mobileBilling.<field>—Per definitionOther fields in the provider's inputFields definition; see Method fields. Fields that are not in the definition cannot be sent
preauthbooleanNotrue: pre-authorization; the provider 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 mobileBilling 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.

buyer.gsmNumber is used when mobileBilling.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: 8d4f2c1e-0a7b-4b3c-9e5d-6f1a2b3c4d5e" \
  -H "Digest: SHA-256=..." \
  -H "X-Signature: ..." \
  -H "Idempotency-Key: ORDER-1002-1" \
  -d '{
    "paymentMethod": "MOBILE_BILLING",
    "orderId": "ORDER-1002",
    "amount": 5000,
    "currency": "TRY",
    "basketItems": [
      { "id": "DIGITAL-1", "name": "Dijital ürün", "category1": "Dijital", "itemType": "VIRTUAL", "price": 5000 }
    ],
    "mobileBilling": { "gsmNumber": "+905321234567" }
  }'

Response and action

{
  "status": "partial",
  "code": "CUSTOMER_ACTION_REQUIRED",
  "message": "…",
  "data": {
    "paymentId": "7b0e4c2a-1f3d-4e5b-8a6c-9d0e1f2a3b4c",
    "orderId": "ORDER-1002",
    "status": "PROCESSING",
    "paymentMethod": "MOBILE_BILLING",
    "providerCode": "<providerCode>",
    "action": {
      "id": "act_5c2e9f0a1b3d4e6f7a8b9c0d1e2f3a4b",
      "type": "OTP",
      "expiresAt": "2026-10-11T09:03:00.000Z",
      "otp": { "maskedTarget": "53****67" },
      "consent": { "required": true, "url": "https://…" }
    }
  },
  "statusCode": 202,
  "requestId": "…"
}
  • <providerCode> is a placeholder.
  • The code's validity is in action.expiresAt.
  • consent.url is the provider's agreement address; show it to the buyer as a link they can open.

Confirm

POST https://api-gateway.lunixi.com/api/v1/payments/{paymentId}/actions/{actionId}/confirm
{ "otp": "123456", "consentAccepted": true }
OutcomeResponseWhat to do
Payment completed200, data.status CAPTUREDShip the order on payment.captured
No agreement consent422 payment.action.consent_requiredGet the buyer's acceptance and send again with consentAccepted: true. No attempt is used
Code missing or malformed400 payment.action.otp_requiredSend the code as digits only. No attempt is used
Wrong code422 payment.action.input_rejectedThe action is open; remaining attempts in details.attemptsRemaining. There are 3 attempts
No attempts left400 payment.action.attempts_exhaustedThe payment is FAILED; start a new payment
Result being verified with the provider202 PAYMENT_PENDING_RECONCILIATIONDo not start a new payment; wait for the webhook

If the code expires, the payment becomes FAILED and payment.failed is sent.

Resending the code

If the buyer did not receive the code, send a new one with POST /payments/{paymentId}/actions/{actionId}/resend. The response carries a new action; use the new action.id for confirm. A code can be resent at most 3 times per payment, with at least 30 seconds between sends; the rules are the same as in Payment — resend.

Line eligibility and limits

  • If the line is closed to this method, not eligible, or its billing limit is not enough for the amount, the payment is declined: response 400, data.status FAILED. Offer the buyer another method.
  • When more than one provider is connected and you omit providerCode, eligibility is checked with the providers before the payment starts. If none is eligible the response is 422 payment.provider.none_eligible; no payment record is opened.
  • Amount limits and currencies are in the minAmount, maxAmount and currencies fields of the GET /payments/methods response.

Pre-authorization

Pre-authorization support is shown by the preauth field of the GET /payments/methods response. When it is not supported, a request with preauth: true gets 422 payment.preauth.unsupported_for_method before the payment starts; nothing is added to the bill.

Errors

SituationResponse
Line missing or malformed; field not in the definition400 payment.method.block_invalid (details.field: mobileBilling.gsmNumber)
No providerCode and none of the connected providers can take the payment422 payment.provider.none_eligible
The selected provider is not connected422 payment.provider.not_available
Pre-authorization not supported422 payment.preauth.unsupported_for_method
Line not eligible or limit not enough400, payment FAILED

Codes of the action endpoints: Payment — Error codes.

Webhooks

EventWhen
payment.capturedThe amount was added to the bill. 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 (decline, expired code or no attempts left)

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

Next steps