Fraud & Fraud AI
Event Ingest API

Event Ingest API

The Decision API only sees the transaction in front of it. Rules that count over time — "more than 10 transfers today", "more than 5 distinct payees this month", a deviation from your own turnover — need history. You feed that history through these endpoints.

POST /api/v1/fraud/events/transactions         (single)
POST /api/v1/fraud/events/transactions/batch   (batch)

Two endpoints, one integration. You stream every business event to event ingest and call the Decision API at payment time. Velocity, distinct-count and baseline scenarios only fire once the ledger holds data. You do not need a message queue on your side: ingest writes synchronously, so the very next decision already sees the event.

Authentication — per-request signature

⚠️

These two endpoints are not called with a bearer token. The Decision API uses a short-lived bearer token; event ingest requires an Ed25519 signature on every request. The signature is what binds the event to your account — the body cannot claim a different one.

Every request carries these headers:

HeaderRequiredValue
X-Key-Id✔The id of your registered public key
X-Date✔ISO 8601 timestamp. A request more than ±300 seconds from server time returns 401
X-Nonce✔A value unique per request. The same key plus the same nonce a second time returns 401
Digest✔ (when there is a body)SHA-256= followed by the Base64 SHA-256 digest of the body
X-Signature✔Base64 Ed25519 signature of the canonical string

The canonical string joins its lines with \n:

POST
/api/v1/fraud/events/transactions
X-Date:2026-10-05T09:12:44.000Z
X-Nonce:5f1c9a6e-7b2d-4a11-9c3e-2f8a7d41b0c6
Digest:SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
  • The first line is the HTTP method in upper case; the second is the path including the query string.
  • The Digest line is only added when you send a Digest header.
🚫

Compute the digest over compact JSON.

The server verifies the digest by parsing the body it received and re-serialising it — that is, over JSON with no indentation and no whitespace. If you pretty-print the body for readability and digest that form, every request returns 401 INVALID_DIGEST. Key order is preserved as you sent it; only formatting whitespace must go.

Your key must also hold the FRAUD_SERVICE product entitlement, otherwise the endpoint returns 403. If you configured an IP restriction on the key, the request must come from one of those addresses.

Single event

POST /api/v1/fraud/events/transactions

Required fields

FieldTypeMeaning
eventIdstringYour own unique id for the event. This is the deduplication key
eventTypestringA value from the event-type vocabulary (below). Up to 64 characters
subjectTypestringWhat the event is about (transaction, customer, account, mobile_payment …). Up to 64 characters
occurredAtstring | numberWhen the event happened (ISO 8601 or epoch milliseconds)

merchantId is not read from the body. It is stamped from the authenticated key, so a value written into the body is ignored.

Context fields

All optional — send what you have. Omitting a field makes it "unknown".

GroupFields
Outcomeoutcome, failureCode
PartiescustomerId, accountId, counterparty, counterpartyType
Network and deviceip, ipCountry, sourcePort, destPort, deviceFingerprintId, userAgent
TransactionamountMinor, currency, description, cardFingerprint, installment, phone
ContextpaymentId, merchantOrderId, merchantUrl, backUrl, apiKeyId, terminalId, terminalLocation
ClassificationproductContext, operationKind, direction, paymentScheme
Authorised push paymentpayeeConfirmationOutcome, paymentPurpose, payerType, claimRef, relatedEventId
AccountaccountOpenedAt
Sub-merchantsubMerchantRef
Free-formmetadata
  • amountMinor is a minor-unit integer (250000 = ₺2,500.00). Never send decimals.
  • For dictionary fields such as operationKind and direction, a value outside the dictionary returns 400 — so a typo cannot silently miss a rule.
  • relatedEventId is limited to 180 characters.
⚠️

Personal data arrives in the clear and is not stored in the clear. ip, phone, description, counterparty, userAgent and metadata.email enter the request as plain text; the ingest layer turns them into hash columns for lookup and encrypted columns for storage. The raw value is never persisted verbatim.

Counterparty requirement

These event types move value to a counterparty; the request returns 400 without counterparty:

transfer_requested · transfer_completed · withdrawal_requested · withdrawal_completed

When subjectType or productContext carries the value mobile_payment, phone is mandatory: for operator billing the account key is the phone number.

Deduplication

If the same eventId arrives a second time for the same account, the event is not written again; the response reports it with deduplicated: true. This makes retries safe: after a timeout you can resend the same event with the same eventId.

In event ingest the deduplication key is eventId, not an Idempotency-Key header. The Decision API has no deduplication: a second evaluate call for the same transaction produces a second decision record. For billing, the same transactionRef counts as one transaction for 30 days — see Errors, repeated requests and retries.

Example

BODY='{"eventId":"evt_8812","eventType":"transfer_completed","subjectType":"transaction","occurredAt":"2026-10-05T09:12:40.000Z","customerId":"cus_88","counterparty":"TR330006100519786457841326","amountMinor":250000,"currency":"TRY","ip":"203.0.113.7","direction":"outbound"}'
DIGEST="SHA-256=$(printf '%s' "$BODY" | openssl dgst -sha256 -binary | openssl base64 -A)"
 
curl https://api-gateway.lunixi.com/api/v1/fraud/events/transactions \
  -H "Content-Type: application/json" \
  -H "X-Key-Id: $LUNIXI_KEY_ID" \
  -H "X-Date: $(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
  -H "X-Nonce: $(uuidgen)" \
  -H "Digest: $DIGEST" \
  -H "X-Signature: $SIGNATURE" \
  -d "$BODY"

Response

{
  "status": "success",
  "code": "SUCCESS",
  "message": "...",
  "data": {
    "eventId": "evt_8812",
    "id": "a3f1c0d2-...",
    "deduplicated": false,
    "matchedFlowCount": 2
  },
  "statusCode": 200,
  "requestId": "req_01J8ZV..."
}
  • id is returned only when a row was actually written; it is absent on deduplication.
  • matchedFlowCount is the number of flows that matched this event and ran.

Batch

POST /api/v1/fraud/events/transactions/batch

The body is either a JSON array directly or an { "events": [...] } object. A body that is neither returns 400 FRAUD_EVENT_BATCH_INVALID — an empty batch is never silently counted as "0 recorded, success".

⚠️

At most 500 events per batch. Above that, 400 FRAUD_EVENT_BATCH_TOO_LARGE rejects the whole batch; nothing is partially ingested. Split larger feeds into chunks of 500.

A batch is processed serially and can succeed partially: if one event fails validation, the others are still written.

{
  "status": "success",
  "data": {
    "total": 3,
    "inserted": 2,
    "deduplicated": 0,
    "failed": 1,
    "results": [
      { "eventId": "evt_1", "id": "…", "deduplicated": false },
      { "eventId": "evt_2", "id": "…", "deduplicated": false },
      { "eventId": "evt_3", "deduplicated": false, "error": "Event is missing required field(s): occurredAt." }
    ]
  }
}

Always read the results array: the HTTP status can be 200 while failed is greater than zero. Fix the failed events from the error text and resend them with the same eventId.

Event-type vocabulary

An eventType outside the vocabulary returns 400. That is deliberate: if a typo such as transfer_requsted were accepted silently, the rule counting that event would never fire and the gap would stay invisible for months.

Account and session

account_created · account_closed · account_restricted · relationship_terminated · login_succeeded · login_failed · otp_failed · pin_failed · password_changed · phone_changed · email_changed · device_registered

Payment

payment_attempted · payment_completed · payment_failed · refund · chargeback · chargeback_reversed · authentication_completed

Transfer, withdrawal and remittance

transfer_requested · transfer_completed · withdrawal_requested · withdrawal_completed · beneficiary_created · settlement_requested

Bill payment

bill_payment · bill_query

Merchant and account integrity

merchant_iban_changed · domain_changed · api_key_created · api_request

Authorised push payment context

These are not value movements; they are facts around a payment. They do not change existing thresholds and only enter rules that name them explicitly.

payee_confirmation_checked · payee_confirmation_requested · scam_warning_presented · scam_warning_acknowledged · scam_hold_applied · scam_hold_released · app_scam_reported · app_scam_claim_notified

Some fields are mandatory in this family:

EventRequired
payee_confirmation_checkedpayeeConfirmationOutcome + counterparty
payee_confirmation_requestedpayeeConfirmationOutcome + accountId
scam_warning_presented / scam_warning_acknowledgedpaymentId or relatedEventId
scam_hold_applied / scam_hold_releasedpaymentId or relatedEventId
app_scam_reportedpaymentId or claimRef
app_scam_claim_notifiedclaimRef + accountId
account_restrictedaccountId or customerId

A missing field returns 400: if the record does not say which payment a warning belonged to, the question "was a warning shown" cannot be answered later.

For sector-average rules

Rules that measure deviation from your own turnover read only your own history and need no extra field. A sector-average comparison needs the sector breakdown: add the merchant's MCC to events under metadata.mcc. Events without an MCC keep feeding own-history rules; only the sector comparison finds no data.

Errors and quota

Ingest returns a quota rejection with a Retry-After header and the event is not lost: wait for the period in the header and resend with the same eventId. Status codes, error codes and quota headers: Errors, repeated requests and retries.

Next