Fraud & Fraud AI
Decision API

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/evaluate

Every 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)

FieldTypeMeaning
integrationModestringOnly standalone (the default). The field may be omitted; any other value returns 400
productContextstringYour product surface: live_session, payment_form, hosted_payment_form or direct_api
subjectTypestringcard_payment, bank_transfer, wallet_operation, bill_payment … selects which payload you send
paymentRailstringcard, bank, wallet, crypto …
transferDirectionstringinbound / outbound
paymentIdstringYour reference for this transaction
merchantGroupIdsstring[]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.

FieldValuesMeaning
channelstandalone_api (default) · merchant_formstandalone_api for a server-to-server call; merchant_form when you host your own payment form with the Lunixi SDK
surfaceform_gate · transaction · withdrawal_gate · settlementThe decision surface inside the channel
stagepre_authorization · post_authentication · post_hoc · async_event · scheduledThe evaluation moment in the transaction lifecycle
operationKindcard_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_topupThe business operation kind. It resolves collisions subjectType cannot (a bill payment and a payment to a merchant are both wallet payments)
authModeRequested2d · 3dThe authentication mode you want for the transaction
authModeApplied2d · 3dThe mode you actually applied
transactionRefstring (≤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
subjectIdstring (≤180)The id of the subject being evaluated (an order, customer or session id)
subMerchantRefstring (≤180)The reference of the sub-merchant under your account. If you send it together with merchant.merchantRef, the two values must be identical
identityAssertionstringThe 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

FieldTypeFeeds
amountnumber (minor unit)amount thresholds, velocity value, flat-amount concentration (5.4)
currencystringmulti-currency thresholds
is3Dbooleanauthentication posture
countrystringtransaction geography (4.5)
cardCountrystringissuer-country mismatch
installmentCountnumberinstallment anomalies
methodstringpayment method
idstringcard reference (use a PCI-safe token / fingerprint)

customer

FieldTypeFeeds
idstringcustomer-anchored velocity & distinct-counterparty counts (4.1)
emailstringemail signals — untrusted provider, sequential addresses (4.4)
countrystringcustomer geography
isNewbooleanyoung/new-account thresholds (4.2)
groupsstring[]segment scoping
assignedRuleIdsstring[]explicit rule targeting

device

FieldTypeFeeds
ipAddressstringsame-IP multi-customer / multi-IP rules (4.3), linked-account expansion (8.2)
fingerprintIdstringdevice velocity, card→device churn (5.14)
vpnDetected / proxyDetected / torDetectedbooleananonymized-origin risk
trustedDevicebooleanreduces false positives
countrystringdevice 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".

FieldTypeFeeds
channelstringthe surface the session came from (for example app, web)
activeCallDetectedbooleana phone call in progress at payment time
callStartedSecondsBeforenumberhow many seconds before the payment the call started
remoteAccessToolDetectedbooleanpresence of a remote-access tool
screenShareActivebooleanscreen sharing being active
sessionAgeSecondsnumberthe age of the session
scamQuestionnaireobjectanswers to the verification questions put to the customer
priorWarningobjecta 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.

FieldTypeFeeds
merchantRefstringyour sub-merchant identifier (distinct from your merchantId)
mccstringsector-average turnover & segment (5.1)
category / businessModelstringsegment interpretation (night/weekend rules by sector, 5.3)
countrystringmerchant / POS-server country (5.6)
onboardingDateISO datenew-merchant cumulative volume (5.8)
firstTransactionDateISO datefirst-activity window (5.8)
hasFinancialHistorybooleannew-business volume thresholds (5.8)
domainAgeDaysnumberyoung-domain risk (5.6)
registrationCountrystringregistration vs. operation mismatch

bankTransfer — account / wallet transfers

FieldTypeFeeds
amount, currencynumber / stringtransfer thresholds
senderIban / receiverIbanstringdistinct-counterparty counts, collector/distributor accounts (4.1)
senderAccountNumber / receiverAccountNumberstringaccount linkage
senderName / receiverNamestringname-pattern analysis
senderBankCode / receiverBankCodestringcross-bank flow
reference / descriptionstringtransaction-description keyword screening (4.6)
countrystringcorridor geography

billPayment — bill payment & remittance

FieldTypeFeeds
billerId / billerCategorystringbiller concentration, per-month bill counts (Section 6)
invoiceId / subscriberNumberstringduplicate / query-count checks
amount, currency, dueDatenumber / string / datevalue & 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 under metadata.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.