Payment Orchestrator
Installments & BIN

Installments & BIN

As soon as the buyer types the first digits of their card, you can identify the card (brand, card family, issuing bank) and show a real installment table — including any installment surcharge — before the payment is submitted. The centerpiece is quote binding: when you request a specific installment option for a known BIN, the response carries a quoteToken, and the payment charges exactly the amounts that quote displayed. No drift between what the buyer saw and what their card was charged.

All amounts are minor-unit (kuruş) values: request amount is an integer, and quoted prices come back as minor-unit strings — "totalPrice": "10500" means ₺105,00. Never send decimals or floats.

How it works

Identify the card

When 6–8 digits are available, call BIN info to learn brand, family and bank — and whether the card is installment-eligible.

Fetch the installment table

Call BIN installments with the amount (and the BIN, once known). You get the available options (1×, 3×, 6×…), each with its per-month and total amounts.

Display the table

Render the options exactly as returned. The totals already include any installment surcharge — never recompute them client-side.

Bind the chosen option

When the buyer picks an option greater than 1×, call BIN installments again with binOrPan and the chosen installment — the response now has binding: true and a signed quoteToken. Pass it as installmentQuoteToken in the payment request (Card — 2D or Card — 3D Secure). The platform charges the quoted amounts — displayed == charged.

Server-side endpoints (Bearer)

These run on your backend with your at_live_… access token.

POST /bin/info — card lookup

Send the first 6 or 8 digits. If you accidentally send a longer PAN, only the prefix is used and the number is not stored.

curl https://api-gateway.lunixi.com/api/v1/bin/info \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Content-Type: application/json" \
  -d '{ "binOrPan": "54000000" }'
{
  "status": "success",
  "code": "BIN_FOUND",
  "data": {
    "found": true,
    "binNumber": "54000000",
    "bankName": "Example Bank",
    "bankCode": "0001",
    "cardBrand": "MASTERCARD",
    "cardFamily": "World",
    "cardProgram": "",
    "cardType": "CREDIT",
    "cardLevel": "CLASSIC",
    "force3d": false,
    "isCommercial": false,
    "supportedInstallment": true,
    "enabledInstallments": [2, 3, 6, 9]
  }
}

An unknown BIN does not fail the call: you get code: "BIN_PATTERN_FALLBACK" with found: false and brand-pattern-derived fields, so the form never breaks.

GET /bin/card-taxonomy — the reference lists

Returns the live lists of brands, card families and banks the platform knows. Use it to build dropdowns, map BIN results to your own display names, or validate configuration — instead of hardcoding lists that go stale.

curl https://api-gateway.lunixi.com/api/v1/bin/card-taxonomy \
  -H "Authorization: Bearer at_live_5f3e2b1a..."

POST /bin/installments — installment table & binding quote

Send the amount (or an intentId); optionally the BIN. Two modes, one endpoint:

  • Indicative table — amount (+ optional binOrPan): returns the offered options with binding: false and no quoteToken.
  • Binding quote — binOrPan and a single installment > 1: returns binding: true plus a signed quoteToken and expiresAt.

The format parameter controls the shape of the result:

formatReturns
data (default)Raw JSON options — you render your own UI
htmlAdds a ready-to-embed HTML installment table (html field)
mobileA payload shaped for native mobile rendering
curl https://api-gateway.lunixi.com/api/v1/bin/installments \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Content-Type: application/json" \
  -d '{
    "binOrPan": "54000000",
    "amount": 10050,
    "installment": 3,
    "format": "data"
  }'
{
  "status": "success",
  "code": "INSTALLMENT_QUOTE_ISSUED",
  "data": {
    "binding": true,
    "quoteToken": "iq_5b8d03aa…",
    "expiresAt": "2026-07-06T12:20:00Z",
    "methods": {
      "CARD": {
        "installmentDetails": [
          {
            "cardFamily": "World",
            "bankName": "Example Bank",
            "installmentPrices": [
              { "installment": 1, "totalPrice": "10050", "monthlyPrice": "10050", "offered": true, "markupPct": null },
              { "installment": 3, "totalPrice": "10500", "monthlyPrice": "3500",  "offered": true, "markupPct": "4.48" },
              { "installment": 6, "totalPrice": null,    "monthlyPrice": null,    "offered": false, "markupPct": null }
            ]
          }
        ]
      }
    }
  }
}

totalPrice / monthlyPrice are minor-unit amounts as strings; an option that is not offered comes back with offered: false and null prices (never a misleading computed price). markupPct is the surcharge label to display ("+%X vade farkı") — the prices already include it. Multi-installment totals can exceed the base amount — that difference is the installment surcharge, already priced into the quote.

In-form endpoints (checkout form token)

Inside the hosted checkout form session, the same two lookups exist under /form/…, authenticated with the checkout form token — sent in the request body as the token field, not as a header — so the buyer's browser can call them directly:

EndpointSame asDifference
POST /form/bin/info/bin/infoBody-token auth (token field)
POST /form/bin/installments/bin/installmentsBody-token auth (token field); merchant, intent and amount are resolved from the token (the body carries only binOrPan, paymentMethod, installment); no html format
// In the buyer's browser, inside the checkout session
const res = await fetch(
  'https://api-gateway.lunixi.com/api/v1/form/bin/installments',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      token: checkoutFormToken, // auth — from POST /payments/intents
      binOrPan: '54000000',
      installment: 3,           // omit for the indicative table
    }),
  },
);
const quote = await res.json();

Quote binding — displayed == charged

Installment pricing is a promise to the buyer, so the platform makes it contractual:

  1. The quote you fetch is computed against the live provider credentials and surcharge configuration at that moment, and sealed under a quoteToken with an expiry.
  2. You display the options from that quote — never amounts you computed yourself.
  3. The payment request (form/2d, form/3d, direct/payment) carries the token back as installmentQuoteToken.
  4. At charge time the platform validates the token and charges exactly the quoted amounts. If the quote has expired, or it doesn't match the payment (different card BIN, different amount, unavailable option), the payment is rejected — it is never silently re-priced.
⚠️

Never cache installment tables across sessions or recompute surcharges client-side. Rates can change at any time; only the quoteToken guarantees that the buyer is charged what they were shown. If a quote expires while the buyer hesitates, fetch a fresh one and re-render the table.

The amount in the payment request is the basket total (the base amount); 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. Amount fields in responses and webhooks show this charged amount.

Paying without installments? Simply omit installmentQuoteToken — the payment charges the base amount in a single installment.

Errors

SituationWhat you seeWhat to do
BIN too short / not 6–8 usable digits400 validation errorWait until at least 6 digits are typed
Unknown BINBIN_PATTERN_FALLBACK with found: falseFall back to a no-installment (single payment) flow
Card not installment-eligibleOptions come back offered: false (or 1×-only)Show single payment only
Expired quoteToken at charge timePayment rejected — quote no longer validFetch a fresh quote, re-render, resubmit
Quote/payment mismatch (different BIN or amount)Payment rejected — quote does not matchFetch a new quote for the actual card and amount
html format on /form/bin/installmentsIgnored — HTML embed is not available in form scopeRender the data payload with the form's own UI
No amount and no intentId on /bin/installmentsAMOUNT_REQUIREDSend the minor-unit amount (or an intentId)

Webhooks

BIN and quote lookups are read-only and emit no webhooks. The events you receive come from the payment itself (payment.captured, payment.failed, …) — see Webhooks.

Next steps