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, retryAfter on RATE_LIMITED); they are listed with the code below.
  • Every response from a Partner API route — 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.
  • The envelope and X-Request-Id guarantee covers the defined routes. A path that matches no route (a typo, a wrong prefix) gets the web framework's plain 404 body — { "message", "error", "statusCode" } — with no envelope and no X-Request-Id. If you see that shape, check the URL.
  • 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. Rows that were imported but need your attention (for example coordinates far from the pincode) come back in the same response as warnings: [{ index, sourceLoanNumber, reason }] — a row can have several entries; the list is capped at 50, and summary.warned counts the rows that imported with at least one warning. 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. :::

Order of checks​

Each request is checked in this order, and the first failure is returned:

  1. Authorization, timestamp and signature headers present → otherwise 401 UNAUTHORIZED.
  2. A body sent with a Content-Type other than application/json → 415 UNSUPPORTED_MEDIA_TYPE.
  3. Body parsed → malformed JSON is 422 VALIDATION_FAILED (message malformed request body (send valid JSON), no details), an oversized body 413 PAYLOAD_TOO_LARGE.
  4. Key, environment, allowlist, lockout, timestamp and signature → 401 / 403 / 429.
  5. Per-key request rate (60/min) → 429 RATE_LIMITED.
  6. Request schema → 422 VALIDATION_FAILED with details. (The 10-batches/hour limit on POST /cases is checked after this.)

So a 413, 415 or malformed-JSON 422 can reach you before your signature is checked, but a schema 422 never precedes authentication. 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)
404STATEMENT_NOT_FOUNDBilling: no finalized statement for that month (not billed yet, or no such month)Not a fault — list GET /billing/statements to see which months exist
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
409NO_INVOICEBilling: the month is finalized but no invoice number was allocated when it was finalized (invoice is null on that statement). Not transientDo not retry on a loop — contact [email protected]
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_TYPEA request with a body whose Content-Type is not application/json (bodies are signed as raw JSON bytes only). Returned before the signature is checked, and does not count toward the lockoutSend Content-Type: application/json
422VALIDATION_FAILEDBody fails the schema (error.details[]), malformed JSON, a POST …/recall with no body and no Content-Type header (send {}), invalid cursor/since/status query on the poll or delivery-log endpoints, a month that is not YYYY-MM or a limit outside 1–60 on billing, 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. The Retry-After header and error.retryAfter both give the seconds to waitHonour Retry-After; batch more rows per push instead of pushing more often
500INTERNAL_ERROROur fault (also: the billing invoice PDF failed to render, or the sandbox signature debugger could not read your key's secret)Retry 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 while every loan in it still has a live case (reliability)
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 the 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.

The schema covers only the envelope — for POST /cases that is rows (a non-empty array of objects) and the optional fullSnapshot boolean. For example, a body without rows:

422 — schema failure
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "request body failed validation",
"details": [
{ "field": "rows", "reason": "must have required property 'rows'" }
]
}
}

Malformed JSON returns the same code with the message malformed request body (send valid JSON) and no details. The message never quotes your body or the parser's error text.

Everything inside a row — missing or malformed fields, an unparseable loan.dpd or date, a phone that does not normalise to 10 digits, a missing address line, state or coordinates — never produces a 422 (only a row that is not a JSON object at all fails the envelope schema, with field rows.N). Per-row problems come back in a 200 response: rejected rows in errors[], and, for an organisation on a coordinate grace period, missing address fields or coordinates in warnings[] (the row still imports). A ?dryRun=1 call shows exactly the same lists 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
409 NO_INVOICE (billing)No — contact [email protected]

Rate limits​

LimitValueOn breach
Requests60 / minute per API key429 RATE_LIMITED + Retry-After (and error.retryAfter)
POST /cases batches10 / hour per API key — ?dryRun=1 calls and Idempotency-Key replays count toward the 10429 RATE_LIMITED + Retry-After (and error.retryAfter)
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. :::