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:
| Header | Required | Value |
|---|---|---|
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
Digestline is only added when you send aDigestheader.
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/transactionsRequired fields
| Field | Type | Meaning |
|---|---|---|
eventId | string | Your own unique id for the event. This is the deduplication key |
eventType | string | A value from the event-type vocabulary (below). Up to 64 characters |
subjectType | string | What the event is about (transaction, customer, account, mobile_payment …). Up to 64 characters |
occurredAt | string | number | When 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".
| Group | Fields |
|---|---|
| Outcome | outcome, failureCode |
| Parties | customerId, accountId, counterparty, counterpartyType |
| Network and device | ip, ipCountry, sourcePort, destPort, deviceFingerprintId, userAgent |
| Transaction | amountMinor, currency, description, cardFingerprint, installment, phone |
| Context | paymentId, merchantOrderId, merchantUrl, backUrl, apiKeyId, terminalId, terminalLocation |
| Classification | productContext, operationKind, direction, paymentScheme |
| Authorised push payment | payeeConfirmationOutcome, paymentPurpose, payerType, claimRef, relatedEventId |
| Account | accountOpenedAt |
| Sub-merchant | subMerchantRef |
| Free-form | metadata |
amountMinoris a minor-unit integer (250000= ₺2,500.00). Never send decimals.- For dictionary fields such as
operationKindanddirection, a value outside the dictionary returns400— so a typo cannot silently miss a rule. relatedEventIdis 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..."
}idis returned only when a row was actually written; it is absent on deduplication.matchedFlowCountis the number of flows that matched this event and ran.
Batch
POST /api/v1/fraud/events/transactions/batchThe 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:
| Event | Required |
|---|---|
payee_confirmation_checked | payeeConfirmationOutcome + counterparty |
payee_confirmation_requested | payeeConfirmationOutcome + accountId |
scam_warning_presented / scam_warning_acknowledged | paymentId or relatedEventId |
scam_hold_applied / scam_hold_released | paymentId or relatedEventId |
app_scam_reported | paymentId or claimRef |
app_scam_claim_notified | claimRef + accountId |
account_restricted | accountId 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
- Decision API — the decision at payment time
- Decision Logs — reading and reconciling the decisions that were made
- Flows and Resolution — which flow is triggered by which event