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)
code | Cause |
|---|---|
VALIDATION_ERROR | A 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_DECLARABLE | A channel or product context belonging only to Lunixi's own services was declared (for example productContext: hosted_payment_form) |
FRAUD_SUB_MERCHANT_REF_MISMATCH | subMerchantRef and merchant.merchantRef differ. They are the same concept; which sub-merchant is being evaluated cannot stay ambiguous |
FRAUD_SUB_MERCHANT_REF_INVALID | The sub-merchant reference does not match the format (letters, digits and . _ : @ -; up to 180 characters) |
FRAUD_ASSERTION_CHANNEL_INVALID | identityAssertion can only be sent with channel: merchant_form |
FRAUD_ASSERTION_SUBJECT_REQUIRED | identityAssertion 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)
code | Cause |
|---|---|
FRAUD_EVENT_INVALID | A 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_INVALID | The batch body is neither an array nor { "events": [...] } |
FRAUD_EVENT_BATCH_TOO_LARGE | The 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)
code | Status | Cause |
|---|---|---|
AUTH_HEADERS_MISSING | 401 | X-Key-Id, X-Date, X-Nonce or X-Signature is absent |
INVALID_DATE | 401 | X-Date is outside the five-minute window |
NONCE_REPLAY | 401 | The same key plus the same nonce was used a second time |
INVALID_KID | 401 | X-Key-Id is not recognised |
DIGEST_MISSING | 401 | A request with a body has no Digest header |
INVALID_DIGEST | 401 | The digest does not match the body — almost always caused by signing pretty-printed JSON |
INVALID_SIGNATURE | 401 | The signature over the canonical string could not be verified |
MERCHANT_SUSPENDED | 403 | The account is not active |
IP_NOT_ALLOWED | 403 | The request came from outside the IP list configured on the key |
Authorisation and the product gate (403 / 503)
code | Status | Cause |
|---|---|---|
PRODUCT_NOT_ENABLED | 403 | The identity is not entitled to the FRAUD_SERVICE product |
PRODUCT_ENTITLEMENT_UNAVAILABLE | 503 | The product entitlement could not be verified. This is transient; honour Retry-After when present |
PRODUCT_ENTITLEMENT_NOT_RESOLVED | 403 | The token was minted without a product stamp |
fraud.module.not_enabled | 403 | subMerchantRef 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_CALLABLE | 403 | The 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)
code | Cause |
|---|---|
INVALID_CURSOR | The 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)
code | Status | Cause |
|---|---|---|
fraud.quota.exceeded | 429 | The daily quota is exhausted or the request-rate limit was exceeded. details.breach says which |
fraud.quota.unavailable | 503 | The 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.
| Header | Meaning |
|---|---|
Retry-After | How many seconds before retrying |
X-RateLimit-Limit | The rate limit |
X-RateLimit-Remaining | Requests left in the window |
X-RateLimit-Reset | When the daily counter resets (epoch seconds) |
X-Quota-Limit | The daily quota |
X-Quota-Remaining | What is left of the daily quota |
X-Quota-Reset | When 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:
| Endpoint | Repeat behaviour |
|---|---|
POST /fraud/events/transactions | Keyed 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/batch | Each event is deduplicated individually on its own eventId. You can resend the batch as a whole |
POST /fraud/decisions/evaluate | No 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 endpoints | Repeatable by nature |
Retry rules
| Case | Retry |
|---|---|
400 (validation) | No. Fix the request; the same request always yields the same answer |
401 signature / nonce / date | Only with a fresh nonce and a fresh X-Date. Resending the same signature returns NONCE_REPLAY |
403 product / module gate | No. Entitlement is required |
429 | Yes, after waiting out Retry-After |
503 | Yes, 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
- Error Handling — the platform error envelope
- Decision API
- Event Ingest API