Fraud & Fraud AI
Errors and Retries

Errors, repeated requests and retries

The fraud endpoints use the platform's single error envelope. This page collects only the fraud-specific codes, the quota response and the retry rules.

🚫

Branch on code, not on message. message is for humans: it is translated and it changes over time. code is part of the contract.

Error envelope

{
  "status": "failure",
  "code": "FRAUD_CHANNEL_NOT_DECLARABLE",
  "message": "…",
  "statusCode": 400,
  "timestamp": "2026-10-05T09:12:44.816Z",
  "service": "api-gateway",
  "systemTime": 1791277964816,
  "requestId": "req_7f3d9c1a4b2e"
}

requestId is also returned in the X-Request-Id header on every response. That is the value to quote in a support request.

status on a successful response

⚠️

There is no success field in the success envelope. A successful fraud response carries status: "success" at the top level, with the payload under data. A client check that reads response.success always sees undefined — read the status field or the HTTP status code instead.

Decision API — validation errors (400)

codeCause
VALIDATION_ERRORA field has an invalid type or length, or an undocumented top-level field was sent. The field breakdown is returned in details.errors
FRAUD_CHANNEL_NOT_DECLARABLEA channel or product context belonging only to Lunixi's own services was declared (for example productContext: hosted_payment_form)
FRAUD_SUB_MERCHANT_REF_MISMATCHsubMerchantRef and merchant.merchantRef differ. They are the same concept; which sub-merchant is being evaluated cannot stay ambiguous
FRAUD_SUB_MERCHANT_REF_INVALIDThe sub-merchant reference does not match the format (letters, digits and . _ : @ -; up to 180 characters)
FRAUD_ASSERTION_CHANNEL_INVALIDidentityAssertion can only be sent with channel: merchant_form
FRAUD_ASSERTION_SUBJECT_REQUIREDidentityAssertion was sent without subjectId. Without a subject to bind to, the artefact cannot be verified

channel and integrationMode are closed dictionaries. The channels an external caller may declare are merchant_form and standalone_api; integrationMode can only be standalone. The lunixi_checkout and wallet channels come from Lunixi's own services, because it would not be right for flows bound to those channels to be triggered from outside.

Event ingest — validation errors (400)

codeCause
FRAUD_EVENT_INVALIDA required field is missing, eventType is outside the vocabulary, a dictionary field carries an unrecognised value, or a field the event type requires is absent
FRAUD_EVENT_BATCH_INVALIDThe batch body is neither an array nor { "events": [...] }
FRAUD_EVENT_BATCH_TOO_LARGEThe batch holds more than 500 events. The whole batch is rejected

In a batch call, per-event validation is returned in results[].error and the HTTP status stays 200 — always check the failed field. An unexpected internal failure appears as FRAUD_EVENT_INGEST_FAILED for that event; no detail is surfaced.

Authentication (401 / 403)

codeStatusCause
AUTH_HEADERS_MISSING401X-Key-Id, X-Date, X-Nonce or X-Signature is absent
INVALID_DATE401X-Date is outside the five-minute window
NONCE_REPLAY401The same key plus the same nonce was used a second time
INVALID_KID401X-Key-Id is not recognised
DIGEST_MISSING401A request with a body has no Digest header
INVALID_DIGEST401The digest does not match the body — almost always caused by signing pretty-printed JSON
INVALID_SIGNATURE401The signature over the canonical string could not be verified
MERCHANT_SUSPENDED403The account is not active
IP_NOT_ALLOWED403The request came from outside the IP list configured on the key

Authorisation and the product gate (403 / 503)

codeStatusCause
PRODUCT_NOT_ENABLED403The identity is not entitled to the FRAUD_SERVICE product
PRODUCT_ENTITLEMENT_UNAVAILABLE503The product entitlement could not be verified. This is transient; honour Retry-After when present
PRODUCT_ENTITLEMENT_NOT_RESOLVED403The token was minted without a product stamp
fraud.module.not_enabled403subMerchantRef was sent but the sub-merchant / payment-institution module is not enabled on the account. details.module names the module
FRAUD_PANEL_SURFACE_NOT_MACHINE_CALLABLE403The requested endpoint belongs to the fraud operations console and cannot be called with an API key
⚠️

The console surface and the API surface are separate. The fraud endpoints callable with an API key are only the ones documented here: decisions, event ingest and decision logs. The operations console endpoints work with a console session and are closed to machine callers by default.

Pagination (400)

codeCause
INVALID_CURSORThe cursor value is corrupted. There is no silent fall back to the first page: in a reconciliation loop that would mean processing the same page twice

Quota (429 / 503)

codeStatusCause
fraud.quota.exceeded429The daily quota is exhausted or the request-rate limit was exceeded. details.breach says which
fraud.quota.unavailable503The quota backend could not be reached. Returned on event ingest only

The decision path does not stop when the quota backend is unreachable. The quota check is skipped and decisions keep being produced; a payment should not stop because of a counter. On event ingest a 503 is returned instead: you can resend the event, so neither the event is lost nor the quota pierced.

Quota headers

A quota rejection carries these headers. The values are never invented: they all come from the layer that enforces the quota, so when a value is unknown that header is not sent at all.

HeaderMeaning
Retry-AfterHow many seconds before retrying
X-RateLimit-LimitThe rate limit
X-RateLimit-RemainingRequests left in the window
X-RateLimit-ResetWhen the daily counter resets (epoch seconds)
X-Quota-LimitThe daily quota
X-Quota-RemainingWhat is left of the daily quota
X-Quota-ResetWhen the daily quota resets (epoch seconds)

The same values are returned in the response body under details.quota. Do not turn the absence of a header into a guessed limit — that leads to retrying at the wrong rate.

As you approach the quota threshold, the fraud.quota.threshold_reached platform webhook event is sent. Delivery and signing: Webhook setup.

Repeated requests

⚠️

On the fraud endpoints an Idempotency-Key header is not a deduplication mechanism. Deduplication works differently per endpoint:

EndpointRepeat behaviour
POST /fraud/events/transactionsKeyed on eventId. The same account plus the same eventId is not written twice; the response returns deduplicated: true. Retrying is safe
POST /fraud/events/transactions/batchEach event is deduplicated individually on its own eventId. You can resend the batch as a whole
POST /fraud/decisions/evaluateNo deduplication. A second call produces a second decision and a second decision record. For billing, the same transactionRef counts as one transaction for 30 days, but that is not deduplication — the decision is recomputed
GET endpointsRepeatable by nature

Retry rules

CaseRetry
400 (validation)No. Fix the request; the same request always yields the same answer
401 signature / nonce / dateOnly with a fresh nonce and a fresh X-Date. Resending the same signature returns NONCE_REPLAY
403 product / module gateNo. Entitlement is required
429Yes, after waiting out Retry-After
503Yes, after waiting out Retry-After
5xx (other)On event ingest yes, with the same eventId. On a decision call, do not retry automatically: the timing of a decision at payment time belongs to the caller
🚫

Do not retry a decision call in a blind loop. A cardholder is waiting for an answer; endless retries both increase latency and consume your quota. When no decision can be obtained, apply your own business rule (stop the payment, route it to manual review, or continue on your own fallback threshold) and measure that condition on your side.

Next