Decision API
The Fraud product exposes a single real-time decision endpoint. You send a transaction (and whatever context you have about the customer, device and merchant) and receive an allow / review / block / force_3d decision with an explainable reason set — in milliseconds, before you charge.
It is transport- and gateway-agnostic: you do not need to use our payment orchestrator. A merchant with its own gateway or a single POS, or a payment institution (PSP) scoring its own sub-merchants, integrates the same way.
There is one decision path. However you authenticate at the edge (your server, panel, SDK), every caller reaches the same engine — so the field contract on this page is the whole contract.
Base URL for all examples: https://api-gateway.lunixi.com/api/v1.
Authentication
Fraud decisions use a Bearer access token. Obtain a short-lived token by signing a request to POST /api/v1/auth/token with your registered Ed25519 key, then send it on every decision call:
Authorization: Bearer <access-token>The signing scheme (X-Key-Id, X-Date, X-Nonce, Digest, X-Signature) is described in Authentication. Your token must carry the fraud:view_decisions scope. The authenticated party (your merchantId) is taken from the token, never from the request body — the body cannot spoof a different tenant.
Endpoint
POST /api/v1/fraud/decisions/evaluateEvery field below is optional — send what you have. The engine reads exactly the fields your active rules reference; anything else is stored as context. All amounts are minor-unit integers (e.g. 250000 = ₺2.500,00). Never send decimals.
Send only the top-level fields documented here. The API rejects unknown top-level fields with 400. The nested fields inside each object are free-form, so you can add your own keys inside payment, customer, metadata, etc. without error.
A missing or wrongly typed field does not silently make a rule false — it can make the rule FIRE.
The rule evaluator is three-valued: true · false · indeterminate. If a condition's operands are missing or their types do not line up (for example sending null instead of "1500" to a numeric threshold), the result is indeterminate and, on a fail-closed principle, is treated as matched. The reason text notes this explicitly.
The practical consequence: omitting a field does not mean "that rule won't run" — it can cause the rule to fire. If you do not want a rule to run, narrow its scope; do not try to silence it by leaving a field empty.
Routing fields (top level)
| Field | Type | Meaning |
|---|---|---|
integrationMode | string | Only standalone (the default). The field may be omitted; any other value returns 400 |
productContext | string | Your product surface: live_session, payment_form, hosted_payment_form or direct_api |
subjectType | string | card_payment, bank_transfer, wallet_operation, bill_payment … selects which payload you send |
paymentRail | string | card, bank, wallet, crypto … |
transferDirection | string | inbound / outbound |
paymentId | string | Your reference for this transaction |
merchantGroupIds | string[] | Group scoping for rule targeting |
Channel taxonomy (top level)
These fields say where and at which moment the request came from. Flow triggers match on these dimensions, the decision record keeps them as columns, and reports break down by them. All are optional, but sending them is what lets you narrow your flows.
| Field | Values | Meaning |
|---|---|---|
channel | standalone_api (default) · merchant_form | standalone_api for a server-to-server call; merchant_form when you host your own payment form with the Lunixi SDK |
surface | form_gate · transaction · withdrawal_gate · settlement | The decision surface inside the channel |
stage | pre_authorization · post_authentication · post_hoc · async_event · scheduled | The evaluation moment in the transaction lifecycle |
operationKind | card_sale · card_preauth · bill_payment · merchant_payment · qr_payment · outbound_transfer · inbound_transfer · w2w_transfer · w2iban_withdrawal · bulk_payout · agent_cash_out · card_topup · agent_topup · bank_transfer_topup | The business operation kind. It resolves collisions subjectType cannot (a bill payment and a payment to a merchant are both wallet payments) |
authModeRequested | 2d · 3d | The authentication mode you want for the transaction |
authModeApplied | 2d · 3d | The mode you actually applied |
transactionRef | string (≤180) | Your own id for the transaction within your account (an order number, for example). Evaluations for the same transaction count as one transaction for 30 days; without it, every call counts as a separate transaction |
subjectId | string (≤180) | The id of the subject being evaluated (an order, customer or session id) |
subMerchantRef | string (≤180) | The reference of the sub-merchant under your account. If you send it together with merchant.merchantRef, the two values must be identical |
identityAssertion | string | The signed identity artefact produced by the Lunixi SDK in the browser |
channel is a closed dictionary and only two values are declarable. The lunixi_checkout and wallet channels come from Lunixi's own services; declared from outside, the request returns 400 FRAUD_CHANNEL_NOT_DECLARABLE. The same applies to productContext: contexts belonging to the platform's internal lanes (simulation, post_hoc_backfill, manual_review) cannot be declared from outside.
On the merchant_form channel three fields travel together. identityAssertion can only be sent with channel: merchant_form, and subjectId is mandatory alongside it — without a subject for the artefact to bind to, verification would settle for "the signature held". The subjectId value must match the one you passed when starting the SDK session; if they diverge the decision still stands, but the record is marked with the assertion_subject_mismatch reason code and the artefact is treated as untrusted.
You do not need to send tenantId. The tenant identity is stamped from the authentication token only; a value in the body is ignored.
stage: post_authentication requires an explicit declaration. The general rule for flow triggers is "a flow that declares no dimension matches every request". At this stage the rule is inverted: a flow does not run in that lane unless it declares the stage explicitly. That way your flows that are published today without dimensions do not run a second time by themselves on the 3DS return.
Object dictionary
Send the object(s) that match your subjectType. The Feeds column names the risk scenario the field supports (TCMB references map to the Turkish Central Bank Risk Management Guide; the same fields drive equivalent rules in other regulatory packs).
payment — card payment
| Field | Type | Feeds |
|---|---|---|
amount | number (minor unit) | amount thresholds, velocity value, flat-amount concentration (5.4) |
currency | string | multi-currency thresholds |
is3D | boolean | authentication posture |
country | string | transaction geography (4.5) |
cardCountry | string | issuer-country mismatch |
installmentCount | number | installment anomalies |
method | string | payment method |
id | string | card reference (use a PCI-safe token / fingerprint) |
customer
| Field | Type | Feeds |
|---|---|---|
id | string | customer-anchored velocity & distinct-counterparty counts (4.1) |
email | string | email signals — untrusted provider, sequential addresses (4.4) |
country | string | customer geography |
isNew | boolean | young/new-account thresholds (4.2) |
groups | string[] | segment scoping |
assignedRuleIds | string[] | explicit rule targeting |
device
| Field | Type | Feeds |
|---|---|---|
ipAddress | string | same-IP multi-customer / multi-IP rules (4.3), linked-account expansion (8.2) |
fingerprintId | string | device velocity, card→device churn (5.14) |
vpnDetected / proxyDetected / torDetected | boolean | anonymized-origin risk |
trustedDevice | boolean | reduces false positives |
country | string | device geography |
session — the session the payment was made in
Authorised push payment signals (a payment talked through on the phone) are read from here. If you do not send it, these signals count as "unknown".
| Field | Type | Feeds |
|---|---|---|
channel | string | the surface the session came from (for example app, web) |
activeCallDetected | boolean | a phone call in progress at payment time |
callStartedSecondsBefore | number | how many seconds before the payment the call started |
remoteAccessToolDetected | boolean | presence of a remote-access tool |
screenShareActive | boolean | screen sharing being active |
sessionAgeSeconds | number | the age of the session |
scamQuestionnaire | object | answers to the verification questions put to the customer |
priorWarning | object | a warning shown to the customer earlier |
merchant — sub-merchant context (PSP / own-gateway)
Send this when you own the merchant relationship (e.g. a payment institution scoring its sub-merchants). If you use our SaaS platform and omit it, MCC is resolved from the merchant's credential.
| Field | Type | Feeds |
|---|---|---|
merchantRef | string | your sub-merchant identifier (distinct from your merchantId) |
mcc | string | sector-average turnover & segment (5.1) |
category / businessModel | string | segment interpretation (night/weekend rules by sector, 5.3) |
country | string | merchant / POS-server country (5.6) |
onboardingDate | ISO date | new-merchant cumulative volume (5.8) |
firstTransactionDate | ISO date | first-activity window (5.8) |
hasFinancialHistory | boolean | new-business volume thresholds (5.8) |
domainAgeDays | number | young-domain risk (5.6) |
registrationCountry | string | registration vs. operation mismatch |
bankTransfer — account / wallet transfers
| Field | Type | Feeds |
|---|---|---|
amount, currency | number / string | transfer thresholds |
senderIban / receiverIban | string | distinct-counterparty counts, collector/distributor accounts (4.1) |
senderAccountNumber / receiverAccountNumber | string | account linkage |
senderName / receiverName | string | name-pattern analysis |
senderBankCode / receiverBankCode | string | cross-bank flow |
reference / description | string | transaction-description keyword screening (4.6) |
country | string | corridor geography |
billPayment — bill payment & remittance
| Field | Type | Feeds |
|---|---|---|
billerId / billerCategory | string | biller concentration, per-month bill counts (Section 6) |
invoiceId / subscriberNumber | string | duplicate / query-count checks |
amount, currency, dueDate | number / string / date | value & timing anomalies |
Other subject-type payloads
walletOperation (walletId, operationType, amount, destinationWalletId), cryptoTransfer (asset, walletAddress, chain, travelRuleReference), loanApplication (requestedAmount, declaredIncome, employerName) and payout (beneficiaryId, destinationType, destinationReference) follow the same shape — send the object that matches your subjectType; your flow's rules decide which fields matter.
telemetry, mobileApp, metadata
telemetry— behavioral signals (mouseMovementCount,copyPasteCount,typingCadenceScore,behavioralRiskScore).mobileApp— mobile identity & attestation (installId,deviceId,platformScopedIdHash,playIntegrityToken,appAttestAssertion…).metadata— any free-form context. Server-derived signals (transaction-description keyword flags, back-URL mismatch, IP country, phone hash) are computed at event ingest and exposed to rules undermetadata.fraudTransactionEvent.*.
Response
{
"status": "success",
"code": "SUCCESS",
"message": "…",
"data": {
"decision": "allow | review | block | force_3d",
"score": 0.42,
"reasonCodes": ["TCMB_3.1.2_6"],
"matchedRuleGroups": [],
"matchedRules": [],
"flowPath": [],
"actions": [],
"tags": [],
"explainability": { "summary": "", "groupResults": [] },
"liveSignals": {},
"multiFlow": {
"enabled": true,
"executedFlows": [],
"skippedFlows": [],
"asyncFlows": []
},
"traceId": "…",
"latencyMs": 12
},
"statusCode": 200,
"requestId": "req_01J8ZV..."
}There is no success field in the response envelope. Success is carried in the top-level status field, with the payload under data. A client check that reads response.success always sees undefined.
Treat decision as authoritative. reasonCodes and explainability back every decision for audit. The multiFlow block shows which flows ran, which were skipped and which were queued in the background — details: Flows and Resolution.
traceId is the decision's unique id. It is the only value you need to store to read the decision later or quote it in a support request — see Decision Logs.
Error codes, the quota response and the retry rules: Errors, repeated requests and retries.
Example
curl https://api-gateway.lunixi.com/api/v1/fraud/decisions/evaluate \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{
"subjectType": "card_payment",
"paymentRail": "card",
"paymentId": "pi_123",
"payment": { "amount": 250000, "currency": "TRY", "id": "hmac:card-fingerprint" },
"customer": { "id": "cus_88", "email": "[email protected]", "isNew": true },
"device": { "fingerprintId": "dev_9f2c", "ipAddress": "203.0.113.7" },
"merchant": { "merchantRef": "sub_88", "mcc": "7995", "country": "TR", "onboardingDate": "2026-06-01" }
}'Feed history for rules that count
A single evaluate call knows only the transaction in front of it. Rules that count over time — "more than 10 transfers today", "more than 5 distinct recipients", a turnover deviation from the sector average — need history. You feed that history to the event backbone:
POST /api/v1/fraud/events/transactions (single)
POST /api/v1/fraud/events/transactions/batch (batch)These endpoints use a per-request Ed25519 signature instead of a bearer token, and they write synchronously: the next evaluate call sees the event immediately. You do not need a message queue on your side.
Required fields, the event-type vocabulary, batch limits and the signing details: Event Ingest API.
Two calls, one integration. Stream every transaction to event ingest for history, and call decisions/evaluate at payment time for the verdict. Velocity, distinct-count and baseline scenarios only fire once the ledger holds data.