Payment Orchestrator
2D

Card — 2D

When you send paymentMethod: "CARD" to POST /payments/direct/payment without force3D (or with false), the card is charged from your own server in a single request. There is no bank-side 3D Secure authentication and no redirect; the result (success or decline) comes back in the same response. Use it for saved-card charges, server-side billing and flows where a redirect is not possible. Without 3D authentication there is no liability shift, so pair 2D charges with your own risk controls.

If you use the Checkout Form, this page is not for you; the form charges the card → 2D Form · 3D Form.

All amounts are minor-unit integers (kuruş): "amount": 10050 means ₺100,50. Never send decimals or floats.

⚠️

PCI DSS required. You receive and send the full card number (PAN) on your server, which puts your systems in PCI DSS scope (SAQ D). Unless you are certified to handle card data, use the Checkout Form. Charging a previously saved card needs no PAN; see Charging a saved card.

How it works

Send the signed request

Call POST /payments/direct/payment from your server with paymentMethod: "CARD", the order fields and card. The request carries Step-Up authentication and an Idempotency-Key.

The platform checks, routes and charges

The transaction passes risk and limit checks, is routed to a suitable provider and is charged immediately.

Read the synchronous result

The response carries the final state: CAPTURED on success, FAILED on decline. If a risk or limit rule turned the payment into 3D Secure, the response carries a REDIRECT action instead; see When a 2D request becomes 3D.

Confirm with the webhook

payment.captured or payment.failed is delivered to your webhook endpoint as the durable record.

Authentication

The endpoint requires Step-Up: your Authorization: Bearer at_… token and an Ed25519 signature over the request. Every request carries these headers:

HeaderValue
AuthorizationBearer at_test_… (test) or Bearer at_live_… (live)
X-Key-IdId of your registered Ed25519 public key
X-DateISO 8601 request time; within ±5 minutes of server time
X-NonceUnique, single-use value per request
DigestSHA-256=<base64> hash of the raw JSON body
X-SignatureBase64 Ed25519 signature over the canonical string
Idempotency-KeyRequired, at most 200 characters; see Idempotency

How to build the signature is described in Authentication. The endpoint requires the Payment Gateway product to be enabled on your account.

Request

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

Card fields

FieldTypeRequiredDescription
paymentMethodstringYesCARD
force3DbooleanNoOmit it or send false for 2D; true asks for 3D Secure → Card — 3D Secure
cardobjectYesCard details; see card
installmentintegerNoNumber of installments, default 1. 2 or more requires installmentQuoteToken
installmentQuoteTokenstringNoBinding token from the installment quote; see Installments
callbackUrlstringNohttp or https URL, at most 2048 characters. If a risk or limit rule turns the payment into 3D Secure, the buyer's browser is POSTed here with the result
preauthbooleanNotrue: the amount is held and the payment becomes AUTHORIZED; see Pre-authorization. When omitted or false the amount is charged immediately

card, force3D, installment and installmentQuoteToken can only be sent with paymentMethod: "CARD". For card, routing chooses the provider; providerCode is not sent.

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.

card

FieldTypeRequiredDescription
cardHolderNamestringRaw cardName as printed on the card
cardNumberstringRaw cardCard number
expireMonthstringRaw cardTwo-digit month, 01–12
expireYearstringRaw cardTwo-digit year. Example: 30
cvcNumberstringRaw card3 or 4 digit security code. Most banks also require it for saved-card charges
cardUserKeystringSaved cardCustomer key the card is stored under
cardTokenstringSaved cardPermanent reference of the saved card (storedCardToken). The Checkout Form's single-use card token cannot be used here
cardSavebooleanNotrue: the card is saved under the customerId key once the payment succeeds. Only allowed on a 3D Secure request (force3D: true); a 2D request gets 400 payment.card.save_requires_3ds
cardNamestringNoA name you give the saved card; used together with cardSave: true. Example: Work card
  • When sent, expireMonth and expireYear must have two digits.
  • For a raw card charge send cardHolderName, cardNumber, expireMonth, expireYear and cvcNumber. For a saved card charge send cardUserKey and cardToken instead of the raw card fields.

cardSave is not sent on a 2D request: cards can only be saved with 3D Secure.

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: 0b9a6c52-6d1e-4f0e-9a51-4c3f9e2d7a10" \
  -H "Digest: SHA-256=..." \
  -H "X-Signature: ..." \
  -H "Idempotency-Key: ORD-84722-1" \
  -d '{
    "paymentMethod": "CARD",
    "amount": 10050,
    "currency": "TRY",
    "orderId": "ORD-84722",
    "customerId": "cust_7788",
    "card": {
      "cardHolderName": "Ada Yilmaz",
      "cardNumber": "5400000000000004",
      "expireMonth": "12",
      "expireYear": "30",
      "cvcNumber": "123"
    }
  }'

Response

A successful charge returns HTTP 200:

{
  "status": "success",
  "code": "PAYMENT_CAPTURED",
  "message": "…",
  "data": {
    "paymentId": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
    "orderId": "ORD-84722",
    "status": "CAPTURED",
    "paymentMethod": "CARD",
    "providerCode": "<providerCode>"
  },
  "statusCode": 200,
  "requestId": "…"
}
FieldDescription
paymentIdPayment id; used for capture, refund and lookup calls
orderIdYour order number
statusCAPTURED; AUTHORIZED for a pre-authorization
paymentMethodCARD
providerCodeCode of the provider that processed the payment; for card, routing chooses the provider. <providerCode> is a placeholder

A declined charge returns HTTP 400. The envelope is status: "failure", the payment is data.status: "FAILED" and code carries the decline reason. When present, details.retryability (RETRYABLE, NON_RETRYABLE, UNKNOWN) tells you whether retrying the same request makes sense.

{
  "status": "failure",
  "code": "RISK_BLOCKED",
  "message": "…",
  "data": {
    "paymentId": "5d1e7a0c-2b9f-4c61-8f3e-0a4b6c8d2e91",
    "orderId": "ORD-84722",
    "status": "FAILED",
    "paymentMethod": "CARD"
  },
  "statusCode": 400,
  "requestId": "…"
}

If the request was rejected before it was processed (validation or authentication error), data is empty and code describes the error.

A charge whose outcome is not yet certain returns HTTP 202: envelope status: "partial", code: "PAYMENT_PENDING_RECONCILIATION", data.status: "NEEDS_RECONCILIATION". The platform settles the outcome by querying the provider and sends a payment.captured or payment.failed webhook. In that case do not open a new charge; wait for the webhook or check GET /payments/:id.

Branch on code; the message text may change. For the envelope fields see Error Handling.

When a 2D request becomes 3D

The 3D Secure decision is your force3D choice. The 3D settings defined for the Checkout Form (force3D on the intent, settings.threeD and the panel 3D policy) do not apply to this request.

There are two exceptions. The risk evaluation or a limit rule configured on your account may require the payment to go through 3D Secure. The response is then a 3D response instead of CAPTURED: HTTP 202, envelope status: "partial", code: "AWAITING_3D", data.status: "AWAITING_3D", and data.action is a REDIRECT action.

Handle it exactly like the 3D Secure flow: send the buyer's browser to action.redirect.url; the result arrives at your callbackUrl and your webhook. For the response fields, see Card — 3D Secure › Response; for the fields of the result posted to your callbackUrl and how to handle it, see the signed callback.

⚠️

If the buyer is not present (server-side billing) you cannot redirect. In that case do not treat the order as paid, and ask the buyer to complete the payment while present. A payment that is never redirected is closed as FAILED after a while and payment.failed is sent. If the buyer is present, send a callbackUrl so that a payment that turns into 3D returns to your page together with the buyer.

When the provider cannot do 2D

If the agreement of the provider routing your payment does not cover 2D (non-secure) sales, the charge cannot complete as 2D. A 2D charge started from your server is treated as customer-not-present (merchant-initiated) and is not upgraded to 3D automatically:

  • If no 2D-capable candidate provider remains, the payment becomes FAILED with the decline code NO_2D_CAPABLE_PROVIDER.
  • This outcome is permanent. Sending the same request again gives the same result; subscription or billing retries must treat it as a permanent decline.
  • Take the payment with 3D Secure while the buyer is present.

Agreement capabilities (supported_operations)

Each provider credential can be limited to the operations its agreement allows. This is a permanent agreement fact on the credential and is managed in the credential form in the panel.

OperationMeaning
PURCHASE2D (non-secure) sale
PURCHASE_3D3D Secure sale (3DS and 3D are also accepted)
AUTHORIZEPre-authorization
CAPTURECapturing an authorization
REFUND / PARTIAL_REFUNDFull / partial refund
VOIDCancel
TOKENIZECard storage (TOKENIZATION is also accepted)

If supported_operations is empty (null) there is no restriction. When a list is set, only the listed operations are performed. The "Disable 2D (non-secure) sales — 3D only" setting in the routing restrictions gives the same result temporarily; the difference is that it is a reversible operational decision.

Charging a saved card

There is no separate saved-card endpoint. In the same request, put the customer key the card is stored under (cardUserKey) and the saved-card reference (cardToken) into the card object instead of the raw card fields:

{
  "paymentMethod": "CARD",
  "amount": 10050,
  "currency": "TRY",
  "orderId": "ORD-84723",
  "card": {
    "cardUserKey": "cust_7788",
    "cardToken": "9f3c1a7e-…",
    "cvcNumber": "123"
  }
}
  • Cards can only be saved with 3D Secure. Sending card.cardSave: true on a 2D request rejects it with 400 payment.card.save_requires_3ds; save the card with Card — 3D Secure.
  • cardToken is the storedCardToken returned to you when the card was saved. The Checkout Form's single-use card token cannot be used here.
  • For enabling card storage, getting the token and listing cards, see Stored Cards.

Installments

If the buyer picked an installment option, send installment together with the installmentQuoteToken you received from the installment quote.

  • installmentQuoteToken is required when installment is 2 or more.
  • amount stays the basket total; you do not send the installment fee. The system computes the fee from the binding quote and adds it to the charged amount: charged amount = amount + installment fee. What you displayed is exactly what is charged.
  • The amount fields in lookups and webhooks show this charged amount.
  • If the token is missing, expired or does not match the request, the payment becomes FAILED and the response is 400 INSTALLMENT_QUOTE_REJECTED. Fetch a fresh quote and resubmit with a new Idempotency-Key.

Pre-authorization

To hold the amount before charging it, send "preauth": true in the request. When the payment succeeds data.status is AUTHORIZED instead of CAPTURED and payment.authorized is sent. You later charge the amount with capture or remove the hold with void.

  • Providers that do not support pre-authorization are removed from the candidates. If no suitable provider remains, the payment becomes FAILED with decline code PREAUTH_NOT_SUPPORTED. The instruction is never turned into an immediate charge.
  • Capture, partial capture, void, expiry and errors: Pre-authorization, Capture & Void.

Idempotency

Idempotency-Key is required (at most 200 characters). Without it the request gets 400 payment.operation.idempotency_key_required and is not processed. Use a value that identifies the attempt, not the payment (for example ORD-84722-1).

SituationResult
Same key, same requestThe result of the first attempt is returned; no second charge is made
Same key, different request (e.g. different amount)409 payment.idempotency.conflict
First request with the same key still in progress429 payment.idempotency.processing; retry shortly with the same key
⚠️

To retry a declined order with another card, send a new Idempotency-Key. If you reuse the same key, the earlier attempt's result is returned and the new card is not tried.

Errors

SituationResponseWhat to do
Missing, invalid or undefined field400 VALIDATION_ERRORFix the field named in details
A removed amount field was sent400 payment.amount.field_removedSend the amount only in amount
Basket total does not equal amount400 VALIDATION_ERRORMake the items' price add up to amount
No Idempotency-Key400 payment.operation.idempotency_key_requiredSend a key for every attempt
Card save on a 2D request400 payment.card.save_requires_3dsSave the card with Card — 3D Secure
Bearer token missing or expired401 UNAUTHENTICATEDGet a new token
Signature could not be verified401 INVALID_SIGNATUREBuild the canonical string in the order given in Authentication
Nonce already used401 NONCE_REPLAYGenerate a new X-Nonce for every request
Invalid X-Date or Digest401 INVALID_DATE, DIGEST_MISSING, INVALID_DIGESTSee Error Handling
Token and signing key belong to different merchants403 AUTH_MERCHANT_MISMATCHUse keys of the same account
IP not on the allow-list403 IP_NOT_ALLOWEDUpdate the IP list in the panel
Account suspended403 MERCHANT_SUSPENDEDContact support
Product not enabled403 PRODUCT_NOT_ENABLEDCheck that the Payment Gateway product is enabled on your account and your API key
Product entitlement could not be verified503 PRODUCT_ENTITLEMENT_UNAVAILABLETemporary; retry shortly
Installment quote invalid or missing400 INSTALLMENT_QUOTE_REJECTED, payment FAILEDFetch a fresh quote and resubmit with a new Idempotency-Key
No provider supports pre-authorizationPayment FAILED, decline code PREAUTH_NOT_SUPPORTEDNothing was held; see Pre-authorization
Rejected by a limit rule400 payment.limit.exceeded, payment FAILEDCheck your limits
Rejected by the risk evaluation400 RISK_BLOCKED, payment FAILEDDo not fulfil the order
Provider cannot do 2DPayment FAILED, decline code NO_2D_CAPABLE_PROVIDERPermanent decline; take the payment with 3D Secure
Bank decline (insufficient funds, invalid card …)400, payment FAILEDShow the decline; use a new Idempotency-Key for another card
Risk or limit rule required 3D202 AWAITING_3D, REDIRECT actionSee When a 2D request becomes 3D
Outcome not yet certain202 PAYMENT_PENDING_RECONCILIATIONDo not open a new charge; wait for the webhook
Same key, different request409 payment.idempotency.conflictUse a new key for a new attempt
Request with the same key in progress429 payment.idempotency.processingRetry shortly with the same key
Temporary outage503Check the status with GET /payments/:id; retrying with the same Idempotency-Key does not charge twice

Common codes for method and provider selection: Payment — Error codes.

Webhooks

EventWhen
payment.capturedThe charge completed and the funds were captured. Fulfil the order on this event
payment.failedThe charge failed; no funds were captured
payment.authorizedThe amount was held for a pre-authorization (AUTHORIZED); no funds were captured yet. See Pre-authorization

The data of the events carries paymentMethod: "CARD" and the providerCode of the provider that processed the payment. For delivery and signature verification see Webhooks.

Next steps