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.
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:
| Header | Value |
|---|---|
Authorization | Bearer at_test_… (test) or Bearer at_live_… (live) |
X-Key-Id | Id of your registered Ed25519 public key |
X-Date | ISO 8601 request time; within ±5 minutes of server time |
X-Nonce | Unique, single-use value per request |
Digest | SHA-256=<base64> hash of the raw JSON body |
X-Signature | Base64 Ed25519 signature over the canonical string |
Idempotency-Key | Required, 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/jsonCard fields
| Field | Type | Required | Description |
|---|---|---|---|
paymentMethod | string | Yes | CARD |
force3D | boolean | No | Omit it or send false for 2D; true asks for 3D Secure → Card — 3D Secure |
card | object | Yes | Card details; see card |
installment | integer | No | Number of installments, default 1. 2 or more requires installmentQuoteToken |
installmentQuoteToken | string | No | Binding token from the installment quote; see Installments |
callbackUrl | string | No | http 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 |
preauth | boolean | No | true: 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
| 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.
card
| Field | Type | Required | Description |
|---|---|---|---|
cardHolderName | string | Raw card | Name as printed on the card |
cardNumber | string | Raw card | Card number |
expireMonth | string | Raw card | Two-digit month, 01–12 |
expireYear | string | Raw card | Two-digit year. Example: 30 |
cvcNumber | string | Raw card | 3 or 4 digit security code. Most banks also require it for saved-card charges |
cardUserKey | string | Saved card | Customer key the card is stored under |
cardToken | string | Saved card | Permanent reference of the saved card (storedCardToken). The Checkout Form's single-use card token cannot be used here |
cardSave | boolean | No | true: 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 |
cardName | string | No | A name you give the saved card; used together with cardSave: true. Example: Work card |
- When sent,
expireMonthandexpireYearmust have two digits. - For a raw card charge send
cardHolderName,cardNumber,expireMonth,expireYearandcvcNumber. For a saved card charge sendcardUserKeyandcardTokeninstead 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.
| 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: 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": "…"
}| Field | Description |
|---|---|
paymentId | Payment id; used for capture, refund and lookup calls |
orderId | Your order number |
status | CAPTURED; AUTHORIZED for a pre-authorization |
paymentMethod | CARD |
providerCode | Code 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
FAILEDwith the decline codeNO_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.
| Operation | Meaning |
|---|---|
PURCHASE | 2D (non-secure) sale |
PURCHASE_3D | 3D Secure sale (3DS and 3D are also accepted) |
AUTHORIZE | Pre-authorization |
CAPTURE | Capturing an authorization |
REFUND / PARTIAL_REFUND | Full / partial refund |
VOID | Cancel |
TOKENIZE | Card 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: trueon a 2D request rejects it with400 payment.card.save_requires_3ds; save the card with Card — 3D Secure. cardTokenis thestoredCardTokenreturned 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.
installmentQuoteTokenis required wheninstallmentis2or more.amountstays 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
FAILEDand the response is400 INSTALLMENT_QUOTE_REJECTED. Fetch a fresh quote and resubmit with a newIdempotency-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
FAILEDwith decline codePREAUTH_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).
| Situation | Result |
|---|---|
| Same key, same request | The 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 progress | 429 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
| Situation | Response | What to do |
|---|---|---|
| Missing, invalid or undefined field | 400 VALIDATION_ERROR | Fix the field named in details |
| A removed amount field was sent | 400 payment.amount.field_removed | Send the amount only in amount |
Basket total does not equal amount | 400 VALIDATION_ERROR | Make the items' price add up to amount |
No Idempotency-Key | 400 payment.operation.idempotency_key_required | Send a key for every attempt |
| Card save on a 2D request | 400 payment.card.save_requires_3ds | Save the card with Card — 3D Secure |
| Bearer token missing or expired | 401 UNAUTHENTICATED | Get a new token |
| Signature could not be verified | 401 INVALID_SIGNATURE | Build the canonical string in the order given in Authentication |
| Nonce already used | 401 NONCE_REPLAY | Generate a new X-Nonce for every request |
Invalid X-Date or Digest | 401 INVALID_DATE, DIGEST_MISSING, INVALID_DIGEST | See Error Handling |
| Token and signing key belong to different merchants | 403 AUTH_MERCHANT_MISMATCH | Use keys of the same account |
| IP not on the allow-list | 403 IP_NOT_ALLOWED | Update the IP list in the panel |
| Account suspended | 403 MERCHANT_SUSPENDED | Contact support |
| Product not enabled | 403 PRODUCT_NOT_ENABLED | Check that the Payment Gateway product is enabled on your account and your API key |
| Product entitlement could not be verified | 503 PRODUCT_ENTITLEMENT_UNAVAILABLE | Temporary; retry shortly |
| Installment quote invalid or missing | 400 INSTALLMENT_QUOTE_REJECTED, payment FAILED | Fetch a fresh quote and resubmit with a new Idempotency-Key |
| No provider supports pre-authorization | Payment FAILED, decline code PREAUTH_NOT_SUPPORTED | Nothing was held; see Pre-authorization |
| Rejected by a limit rule | 400 payment.limit.exceeded, payment FAILED | Check your limits |
| Rejected by the risk evaluation | 400 RISK_BLOCKED, payment FAILED | Do not fulfil the order |
| Provider cannot do 2D | Payment FAILED, decline code NO_2D_CAPABLE_PROVIDER | Permanent decline; take the payment with 3D Secure |
| Bank decline (insufficient funds, invalid card …) | 400, payment FAILED | Show the decline; use a new Idempotency-Key for another card |
| Risk or limit rule required 3D | 202 AWAITING_3D, REDIRECT action | See When a 2D request becomes 3D |
| Outcome not yet certain | 202 PAYMENT_PENDING_RECONCILIATION | Do not open a new charge; wait for the webhook |
| Same key, different request | 409 payment.idempotency.conflict | Use a new key for a new attempt |
| Request with the same key in progress | 429 payment.idempotency.processing | Retry shortly with the same key |
| Temporary outage | 503 | Check 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
| Event | When |
|---|---|
payment.captured | The charge completed and the funds were captured. Fulfil the order on this event |
payment.failed | The charge failed; no funds were captured |
payment.authorized | The 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
- Card — 3D Secure — charging with bank authentication and saving cards
- Installments & BIN — installment table and binding quote
- Stored Cards — managing saved cards
- Refunds — returning money after capture
- Webhooks — payment events