Skip to main content

Errors & rate limits

Error envelope​

Every 4xx/5xx response from the Partner API has one shape:

Error envelope
{
"success": false,
"error": {
"code": "SIGNATURE_INVALID",
"message": "human-readable explanation"
}
}
  • Branch on error.code, never on error.message — messages are for humans and may be reworded.
  • A few codes add extra fields inside error (details on VALIDATION_FAILED, caseId on DUPLICATE_CONFIRMATION); they are listed with the code below.
  • Every response — success or error — carries an X-Request-Id header. Log it with the call, and quote it when you write to [email protected]; it lets us trace the exact request.
  • Success responses do not share a common envelope: most endpoints return { "success": true, ... } with endpoint-specific fields and GET /health returns a flat object. Only errors share the shape above.

:::note Row failures in POST /cases are not errors A batch never fails because one row is bad. Row-level problems come back inside a 200 response as errors: [{ index, sourceLoanNumber, reason }] (capped at 50 entries — summary.errored is the true count) while the good rows are committed. The envelope above is used only when the whole request is rejected: authentication, rate limits, envelope-level schema failure (422), too many rows (413), and so on. See the push API. :::

Authentication runs before schema validation: an unauthenticated caller gets 401, never a 422. If your first calls return 401, fix the signature before you look at the body.

Complete code list​

HTTPCodeWhen it occursWhat to do
401UNAUTHORIZEDMissing auth headers; unknown, revoked or expired key; test key on production or live key on sandbox; caller IP not on your allowlistCheck key id + host pair (environments); check the three headers are present; check your egress IP against the allowlist you registered
401SIGNATURE_INVALIDHMAC does not match the canonical stringUse POST /debug/echo-signature on the sandbox — debugging signatures. Stop retrying: 10 failures in 5 minutes lock the key
401TIMESTAMP_SKEWX-Fieldproof-Timestamp outside ±5 minutes of server time (unix milliseconds)Sync clocks (NTP); sign at send time, not at build time; send ms, not seconds
403LENDER_INACTIVEYour organisation is suspendedContact [email protected]
403SANDBOX_ONLYPOST /debug/echo-signature with a live key, or against the production hostUse a test key on https://alpha-gig.fluxusforge.in
403UNKNOWN_LENDERYour key authenticated but is not attached to a configured organisation record (a provisioning problem, not a client bug)Contact [email protected] with the X-Request-Id
403PAYMENT_MODE_MISMATCHPOST /cases/{loan}/payments/confirm while your organisation is not in org_gateway modeConfirm your configured payment mode; Mode A (static QR) organisations never call confirm
404CASE_NOT_FOUNDNo case for that sourceLoanNumber in your organisation. Also returned for another organisation's loan — there is no existence oracleCheck the sourceLoanNumber exactly as you pushed it, and that the batch was committed (not a dryRun)
404DELIVERY_NOT_FOUNDReplay of an eventId that does not exist for your organisation (wrong id, or another organisation's event)Check the id against GET /cases/updates or the delivery log. Events that were never queued for a callback CAN be replayed — the delivery is created on demand (needs an active callback, else 422 CALLBACK_NOT_CONFIGURED)
409DUPLICATE_CONFIRMATIONSame (sourceLoanNumber, reference) already confirmed. error.caseId is includedTreat as success — the money is already booked. Only caseId is returned, not the original booking; read GET /cases/{loan} if you need outstandingAfter
409CASE_ALREADY_COLLECTEDCase is already fully collected — this reference is a second payment for money not owedRefund the borrower from your gateway; do not retry
409AMOUNT_EXCEEDS_OUTSTANDINGamount is more than 101% of the remaining outstandingRefund the excess / adjust on your side; do not retry as-is
409CASE_CLOSEDCase was recalled or written off before this payment arrivedRefund the borrower; the case is terminal
413PAYLOAD_TOO_LARGERequest body exceeds the server's body-size limitSplit the batch; keep batches ≤5,000 rows
413TOO_MANY_ROWSrows has more than 20,000 entriesSplit the batch (do not split a fullSnapshot: true batch — see below)
415UNSUPPORTED_MEDIA_TYPEContent-Type is not application/jsonSend Content-Type: application/json
422VALIDATION_FAILEDBody fails the schema (error.details[]), malformed JSON, invalid cursor/since/status query on the poll or delivery-log endpoints, mode: "cash" or a bad amount/reference/paidAt on confirmFix the request; see the details shape below
422CALLBACK_NOT_CONFIGUREDPOST /webhooks/test when no active callback URL is configured for this environmentConfigure the webhook URL for this environment (sandbox and production are configured separately)
429AUTH_LOCKED10 signature failures within 300 s → the key is locked for 900 s. Retry-After = seconds remainingStop all traffic on the key, debug the signature, wait out Retry-After
429RATE_LIMITED60 requests/min per key exceeded, or POST /cases called more than 10 times per hour per key. Retry-After is setHonour Retry-After; batch more rows per push instead of pushing more often
500INTERNAL_ERROROur faultRetry with backoff. payments/confirm is safe to retry (a retry after a 5xx resumes the booking); a re-sent batch updates existing cases in place and an identical committed batch is a no-op
503FEATURE_DISABLEDExternal-organisation ingestion is switched off platform-wide (POST /cases)Retry later; if it persists, contact [email protected]

422 VALIDATION_FAILED details​

When the body fails schema validation, error.details lists each violation. field is a dotted path into your request body — for a missing required property it is the path of the object that lacks it (or the property name when it is missing at the top level); reason is the validator's message.

422 — schema failure
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "request body failed validation",
"details": [
{ "field": "rows.0.borrower.phone", "reason": "must be string" },
{ "field": "rows.0.loan", "reason": "must have required property 'outstandingAmount'" }
]
}
}

Malformed JSON returns the same code with a message and no details.

Row-content problems that are not schema violations (an unparseable loan.dpd or date, a phone that does not normalise to 10 digits) do not produce a 422 — they surface per row in the 200 response's errors[], and a ?dryRun=1 call shows exactly the same list without writing anything.

The 409 family on payments/confirm​

The four 409 codes are business outcomes, not transport failures — do not put them on a retry loop:

CodeMeaningMoney
DUPLICATE_CONFIRMATIONAlready booked under this referenceNothing to do — success
CASE_ALREADY_COLLECTEDCase was already fully collectedRefund the borrower
AMOUNT_EXCEEDS_OUTSTANDINGAmount more than 101% of what remainedRefund/adjust the excess
CASE_CLOSEDCase was recalled / written off firstRefund the borrower

Details and the full confirm flow: your gateway and reconciliation.

Retry guidance​

ResponseRetry?
Network error, 500 INTERNAL_ERRORYes, with backoff (confirm resumes; batches are idempotent)
503 FEATURE_DISABLEDLater, not on a tight loop — it is a platform switch, not a transient fault
429 RATE_LIMITED, 429 AUTH_LOCKEDOnly after Retry-After — and for AUTH_LOCKED, only after the signature is fixed
401, 403, 404, 413, 415, 422No — the same request will fail the same way. Fix it first
409 (confirm)No — apply the business outcome above

Rate limits​

LimitValueOn breach
Requests60 / minute per API key429 RATE_LIMITED + Retry-After
POST /cases batches10 / hour per API key — ?dryRun=1 calls and Idempotency-Key replays count toward the 10429 RATE_LIMITED + Retry-After
Batch size20,000 rows hard cap; keep ≤5,000 for fast responses413 TOO_MANY_ROWS
Signature failures10 in 300 skey locked 900 s → 429 AUTH_LOCKED + Retry-After

:::warning fullSnapshot and the row cap fullSnapshot: true means "this batch is my complete open book" — active cases absent from it are flagged for ops review. Chunking a snapshot into several batches would flag everything missing from each chunk. Send the snapshot as one batch (≤20,000 rows); if your open book is larger than that, talk to [email protected] before using fullSnapshot. :::

:::tip Budget the batch limit Ten batches an hour is plenty when each batch carries thousands of rows. Test your row shape with one ?dryRun=1 per change, not one per row — dry-runs spend the same budget. :::