Errors & rate limits
Error envelope
Every 4xx/5xx response from the Partner API has one shape:
{
"success": false,
"error": {
"code": "SIGNATURE_INVALID",
"message": "human-readable explanation"
}
}
- Branch on
error.code, never onerror.message— messages are for humans and may be reworded. - A few codes add extra fields inside
error(detailsonVALIDATION_FAILED,caseIdonDUPLICATE_CONFIRMATION); they are listed with the code below. - Every response — success or error — carries an
X-Request-Idheader. 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 andGET /healthreturns 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
| HTTP | Code | When it occurs | What to do |
|---|---|---|---|
| 401 | UNAUTHORIZED | Missing auth headers; unknown, revoked or expired key; test key on production or live key on sandbox; caller IP not on your allowlist | Check key id + host pair (environments); check the three headers are present; check your egress IP against the allowlist you registered |
| 401 | SIGNATURE_INVALID | HMAC does not match the canonical string | Use POST /debug/echo-signature on the sandbox — debugging signatures. Stop retrying: 10 failures in 5 minutes lock the key |
| 401 | TIMESTAMP_SKEW | X-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 |
| 403 | LENDER_INACTIVE | Your organisation is suspended | Contact [email protected] |
| 403 | SANDBOX_ONLY | POST /debug/echo-signature with a live key, or against the production host | Use a test key on https://alpha-gig.fluxusforge.in |
| 403 | UNKNOWN_LENDER | Your 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 |
| 403 | PAYMENT_MODE_MISMATCH | POST /cases/{loan}/payments/confirm while your organisation is not in org_gateway mode | Confirm your configured payment mode; Mode A (static QR) organisations never call confirm |
| 404 | CASE_NOT_FOUND | No case for that sourceLoanNumber in your organisation. Also returned for another organisation's loan — there is no existence oracle | Check the sourceLoanNumber exactly as you pushed it, and that the batch was committed (not a dryRun) |
| 404 | DELIVERY_NOT_FOUND | Replay 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) |
| 409 | DUPLICATE_CONFIRMATION | Same (sourceLoanNumber, reference) already confirmed. error.caseId is included | Treat as success — the money is already booked. Only caseId is returned, not the original booking; read GET /cases/{loan} if you need outstandingAfter |
| 409 | CASE_ALREADY_COLLECTED | Case is already fully collected — this reference is a second payment for money not owed | Refund the borrower from your gateway; do not retry |
| 409 | AMOUNT_EXCEEDS_OUTSTANDING | amount is more than 101% of the remaining outstanding | Refund the excess / adjust on your side; do not retry as-is |
| 409 | CASE_CLOSED | Case was recalled or written off before this payment arrived | Refund the borrower; the case is terminal |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds the server's body-size limit | Split the batch; keep batches ≤5,000 rows |
| 413 | TOO_MANY_ROWS | rows has more than 20,000 entries | Split the batch (do not split a fullSnapshot: true batch — see below) |
| 415 | UNSUPPORTED_MEDIA_TYPE | Content-Type is not application/json | Send Content-Type: application/json |
| 422 | VALIDATION_FAILED | Body 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 confirm | Fix the request; see the details shape below |
| 422 | CALLBACK_NOT_CONFIGURED | POST /webhooks/test when no active callback URL is configured for this environment | Configure the webhook URL for this environment (sandbox and production are configured separately) |
| 429 | AUTH_LOCKED | 10 signature failures within 300 s → the key is locked for 900 s. Retry-After = seconds remaining | Stop all traffic on the key, debug the signature, wait out Retry-After |
| 429 | RATE_LIMITED | 60 requests/min per key exceeded, or POST /cases called more than 10 times per hour per key. Retry-After is set | Honour Retry-After; batch more rows per push instead of pushing more often |
| 500 | INTERNAL_ERROR | Our fault | 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 |
| 503 | FEATURE_DISABLED | External-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.
{
"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:
| Code | Meaning | Money |
|---|---|---|
DUPLICATE_CONFIRMATION | Already booked under this reference | Nothing to do — success |
CASE_ALREADY_COLLECTED | Case was already fully collected | Refund the borrower |
AMOUNT_EXCEEDS_OUTSTANDING | Amount more than 101% of what remained | Refund/adjust the excess |
CASE_CLOSED | Case was recalled / written off first | Refund the borrower |
Details and the full confirm flow: your gateway and reconciliation.
Retry guidance
| Response | Retry? |
|---|---|
Network error, 500 INTERNAL_ERROR | Yes, with backoff (confirm resumes; batches are idempotent) |
503 FEATURE_DISABLED | Later, not on a tight loop — it is a platform switch, not a transient fault |
429 RATE_LIMITED, 429 AUTH_LOCKED | Only after Retry-After — and for AUTH_LOCKED, only after the signature is fixed |
401, 403, 404, 413, 415, 422 | No — the same request will fail the same way. Fix it first |
409 (confirm) | No — apply the business outcome above |
Rate limits
| Limit | Value | On breach |
|---|---|---|
| Requests | 60 / minute per API key | 429 RATE_LIMITED + Retry-After |
POST /cases batches | 10 / hour per API key — ?dryRun=1 calls and Idempotency-Key replays count toward the 10 | 429 RATE_LIMITED + Retry-After |
| Batch size | 20,000 rows hard cap; keep ≤5,000 for fast responses | 413 TOO_MANY_ROWS |
| Signature failures | 10 in 300 s | key 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.
:::