Payment Orchestrator
Checkout Form

Checkout Form

The hosted checkout form is the recommended way to accept card payments: your server creates a payment intent with POST /payments/intents, the buyer completes payment on Lunixi's form, and card data never touches your infrastructure. This page is the full reference for the intent create — every field, the per-request policy overrides, the response, and what happens in the browser afterwards.

All amounts are integers in minor units (kuruş): "amount": 10050 = ₺100.50. This applies to amount, basketItems[].price and settings.threeD.forceAmountMinor.

How it works

Create an intent (server)

POST /payments/intents with Step-Up auth (Bearer + Ed25519 signature + Idempotency-Key). The response returns a form token valid for ~15 minutes, bound to your allowedOrigin.

Mount the form (browser)

Provide an empty <div> on your page for the form to render into; the SDK fills it.

<div id="lunixi-checkout-form"></div>
 
<script src="https://cdn.lunixi.com/lunixi-checkout.min.js"></script>
<script>
  const checkout = Lunixi.checkout.create({
    type: 'payment',
    target: 'lunixi-checkout-form',
    options: { token: 'the token from the intent response' },
  });
 
  checkout.mount('#lunixi-checkout-form');
</script>

mount() tries getElementById first and falls back to querySelector — 'lunixi-checkout-form' and '#lunixi-checkout-form' do the same thing. You can also pass a DOM element.

⚠️

Leave the target <div> empty and do not give it a fixed height. The SDK clears its contents on mount, and the form's height changes as the card is entered (the instalment table opens) — the SDK handles that itself.

If you only want the card field (single line, narrower PCI scope), use the createCardField variant → Hosted Card Field.

Fraud bootstrap & session proof

The form calls POST /payments/form/live-analyze, which analyzes the session and sets a session-proof cookie. Card-touching calls are rejected without it. Lifecycle signals are streamed via /payments/form/live-event.

BIN & installments

As the buyer types the card number, the form resolves the card's brand and family (POST /form/bin/info) and the installment table (POST /form/bin/installments). The installment quote returns a binding quoteToken — the amount shown to the buyer is exactly the amount charged. See Installments & BIN.

Tokenize

The card fields live in an isolated secure-fields iframe. POST /payments/form/tokenize converts the entered card into a single-use card token (submitted back as paymentCards.secureCardToken) — the PAN is never exposed to the merchant page.

Charge — 2D or 3D

The form calls POST /payments/form/2d or POST /payments/form/3d (with the token, the session proof, and the installmentQuoteToken if installments were chosen). Which one runs is decided by the effective 3D policy — see the force3D section below. The decision is frozen onto the intent at create time; if 3D is required, a direct /form/2d attempt is refused with 409 THREE_D_REQUIRED (the SDK then uses the 3D endpoint).

3D challenge & result

For 3D, the payment moves to AWAITING_3D and the buyer is redirected to GET /payments/threed/:token (public, single-use token) for the bank challenge. Afterwards the callback resolves and the result is delivered to your callbackUrl as an informational form POST (with the _signature + _timestamp platform integrity fields), plus the authoritative webhooks fire — see 3D Form.

3D Secure display

In the panel, under Payments → Form Settings → 3D Secure → 3D Secure display, you choose how the bank's verification screen opens:

OptionWhat happens
Within the form (default)The bank's verification screen opens in a window over the form. When verification finishes, the window closes and the form moves to the next step; the buyer never leaves your page
On a separate pageThe buyer is redirected to the bank's page. The bank returns the buyer to Lunixi's payment continuation page; the remaining steps run there and, when the payment is complete, the buyer returns to your callbackUrl with the result
  • Automatic separate page: If the bank does not allow its verification screen to open in a window, 3D Secure is automatically restarted for the same card on a separate page. You do not need to do anything for this.
  • The choice applies to card verifications in split payments. For single-card payments, the 3D Secure flow is as described on 3D Form.
  • A change affects only payments started afterwards; a payment already in progress completes with the choice it started with.

Continuation page, the fields sent to your callbackUrl and error codes: Split Payments › 3D Secure and the continuation page.

Request

POST /payments/intents — Step-Up auth.

Fields

FieldTypeRequiredDescription
amountinteger (minor units)YesThe single amount field, e.g. 10050 = ₺100.50. It is the basket total: the price of the goods and services the buyer pays for, excluding the installment fee. Min 1. See Amount and installments.
currencystring (ISO 4217)Yese.g. "TRY"
orderIdstringYesYour unique order reference
callbackUrlstringNoWhere the 3D / async result is POSTed (HMAC-signed)
localestringNoForm language. Default "tr".
buyerobjectNoBuyer details — see below
shippingAddressobjectNoAddress object — see below
billingAddressobjectNoAddress object — see below
basketItemsarrayNoBasket lines — see below
paymentGroupstringNoe.g. "PRODUCT"
paymentChannelstringNoe.g. "WEB"
methodenumNoPayment type; default regular. Marketplace split payments use their own endpoint — see Marketplace.
descriptionstringNoCharge description shown to the customer
customerIdstringNoYour customer reference — enables filtering in Listing Payments
metadataobjectNoFree-form key/value data echoed back on the payment
force3DbooleanNoPer-payment 3D switch — overrides every policy, see below
preauthbooleanNotrue → pre-authorization: the amount is held on the buyer's card and the payment becomes AUTHORIZED; you capture or void it later. Omitted or false → the amount is charged immediately. See below
settingsobjectNoPer-request form-settings override — see below

buyer object

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

Length limits on the Checkout Form and where buyer details are used: Buyer.

shippingAddress / billingAddress object

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 which one is used when: Addresses.

basketItems[] object

FieldRequired within itemExample
idYes"BI101"
priceYes — integer, minor units10050
nameYes"Binocular"
category1Yes"Collectibles"
category2No"Accessories"
itemTypeYes — "PHYSICAL" or "VIRTUAL""PHYSICAL"

The sum of the items' price must equal amount. Item rules, error field paths such as basketItems.0.price and the optional insurance fields: Basket Items.

Amount and installments

  • amount is the single amount field. Send the amount only in amount; the removed amount fields get 400 payment.amount.field_removed.
  • The intent has no installment field; sending one gets 400 VALIDATION_ERROR. The buyer picks the installment in the form.
  • You do not send the installment fee. The system computes it from the binding installment quote the buyer picked and adds it to the charged amount: charged amount = amount + installment fee.
  • Amount fields in responses and webhooks (data.amount, capturedAmountMinor …) show the charged amount; for an installment payment the installment fee is included.

force3D — the top-level override

force3D is the per-payment 3D switch:

  • force3D: true → this payment will run 3D Secure — regardless of the panel policy and regardless of anything in settings.threeD.
  • force3D: false → this payment will not be forced into 3D by policy.
  • Omitted → the effective policy decides (panel settings, optionally overridden per-request by settings.threeD).

An explicit force3D overrides everything — it beats the panel 3D policy and the settings.threeD override, including the amount threshold.

preauth — pre-authorization

Send "preauth": true when you create the form to take this payment as a pre-authorization:

  • When the buyer completes the payment on the form, the amount is held on the card, not charged; the payment becomes AUTHORIZED and the payment.authorized webhook fires.
  • Whatever charge the form makes (2D, 3D Secure or a non-card method) follows this decision; you send nothing else from the form.
  • The form shows only payment methods that support pre-authorization.
  • You later capture the amount or void the hold; a hold not captured in time becomes EXPIRED.
{
  "amount": 10050,
  "currency": "TRY",
  "orderId": "ORDER-1001",
  "callbackUrl": "https://merchant.example.com/payment/callback",
  "preauth": true
}

Window, supported methods and errors: Pre-authorization, Capture & Void.

settings — per-request form-settings override

settings applies on top of your merchant-panel form settings for this intent only. The override rules differ per field:

FieldTypeBehavior
settings.paymentMethodsstring[] — e.g. ["card"]The methods shown on this form. Narrows only: the effective set is the intersection with your panel set — a method disabled in the panel cannot be enabled here.
settings.threeD.mode"2d" | "3d" | "auto"The 3D mode for this intent. Full replacement: if settings.threeD is present, it replaces the panel's 3D configuration for this intent (your signed per-customer decision). Ignored entirely if force3D is set.
settings.threeD.forceAmountMinorstring (minor units)Amount threshold: at or above this amount the payment is forced into 3D. "0" = every payment 3D; null = no threshold. Replaced together with mode when settings.threeD is sent.
settings.buyerProtection.enabledbooleanWhether Buyer Protection runs on this payment. Full replacement: this per-intent decision replaces the panel default — sending true shows the Buyer Protection option on this form even if the panel default is off. See the box below.

Security-sensitive form settings (identityCheck, cardStorage, installmentTab) cannot be overridden per request — they always come from the panel.

What is Buyer Protection? Buyer Protection is an approval-link flow: the buyer confirms the purchase through a confirmation link, adding an explicit buyer-consent step to the payment. It is not 3D Secure and not a replacement for it — 3D is a bank-side authentication; Buyer Protection is a Lunixi-side confirmation layer. The two are configured independently.

Example request

curl -X POST "https://api-gateway.lunixi.com/api/v1/payments/intents" \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Content-Type: application/json" \
  -H "X-Key-Id: key_01JZ8H2M4Q" \
  -H "X-Date: 2026-07-06T12:00:00.000Z" \
  -H "X-Nonce: 9f8d2c4e-1a3b-4c5d-8e7f-0a1b2c3d4e5f" \
  -H "Digest: SHA-256=k6I5cUt...=" \
  -H "X-Signature: MEUCIQDx...==" \
  -H "Idempotency-Key: order-ORD-9988-attempt-1" \
  -d '{
    "amount": 10050,
    "currency": "TRY",
    "orderId": "ORD-9988",
    "callbackUrl": "https://shop.example.com/payment/result",
    "customerId": "c_98765432",
    "description": "Order #9988 payment",
    "buyer": {
      "name": "John", "surname": "Doe",
      "identityNumber": "12345678901",
      "email": "[email protected]", "gsmNumber": "+905350000000",
      "city": "İstanbul", "country": "Turkey",
      "zipCode": "34732", "ip": "85.34.78.112"
    },
    "basketItems": [
      { "id": "BI101", "price": 10050, "name": "Binocular",
        "category1": "Collectibles", "itemType": "PHYSICAL" }
    ],
    "settings": {
      "paymentMethods": ["card"],
      "threeD": { "mode": "auto", "forceAmountMinor": "500000" },
      "buyerProtection": { "enabled": true }
    }
  }'

Response

201 Created:

{
  "status": "success",
  "code": "CHECKOUT_INITIALIZED",
  "data": {
    "paymentId": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "checkoutFormContent": "<script>…</script>"
  }
}
FieldDescription
paymentIdThe payment id — use it for GET /payments/:id, capture, void, refund
tokenThe form token. Hand it to the checkout SDK in the browser. Single-purpose.
checkoutFormContentA ready-to-inject bootstrap <script> that loads and mounts the hosted checkout SDK with this token

The token is origin-bound (to metadata.allowedOrigin / your callbackUrl origin) and expires — 15 minutes by default. Expired token → create a new intent.

Errors

StatusCaseWhat to do
400Validation failed (missing amount/currency/orderId, non-integer amount, malformed settings)Fix the field named in the error message; remember amounts are minor-unit integers
401Missing/invalid Step-Up headers, expired X-Date, reused nonce, bad signatureSee Authentication errors
403Bearer key and signing key mismatch (AUTH_MERCHANT_MISMATCH)Use keys of the same merchant & environment
401 (form calls)Form token invalid or expired (~15 min)Create a fresh intent and remount the form
409 THREE_D_REQUIRED (on /payments/form/2d)3D is required for this payment (by force3D, threshold or mode) but a 2D charge was attemptedFollow the 3D path — the form SDK does this automatically
3D redirect returned from /payments/form/2d (customer-present SALE)The selected provider/credential cannot do 2D, so the charge was auto-escalated to 3DS (escalationReason: "PROVIDER_2D_UNSUPPORTED")Handle the 3D redirect fields — see 2D Form › provider capability
Duplicate Idempotency-KeyThe original response is returned, no second chargeExpected behavior — safe to retry

Webhooks

The checkout form flow emits, depending on outcome:

  • payment.captured — charge completed, funds captured (2D, or 3D after the challenge)
  • payment.authorized — funds reserved, for authorize-then-capture setups
  • payment.failed — final decline (includes structured decline detail)
  • payment.expired — an authorize-only hold expired before it was captured

Delivery and signature verification: Webhooks.

Adding insurance

You can offer your buyer insurance from the provider you have an agreement with, together with the product, in the Checkout Form. The payment request has no separate insurance parameter:

  • You turn insurance on in the panel: in the Insurance menu you turn on insurance and the type you will offer, and under Form Settings > Insurance you choose where it appears in the form. If these settings are off, insurance never appears in the form; steps: Insurance › Setup.
  • The optional insurance fields in your basket items determine which insurance is offered; see Basket Items › Insurance fields.
  • Your payments without insurance are not affected. If you send no insurance fields, or insurance cannot be offered, the payment proceeds as usual.

How the product works and which path to choose: Insurance.

Next steps