Skip to main content
The API returns structured errors with stable codes. Branch on the code field, not the human-readable message.

Error envelope

Any non-2xx response has the following body:
The HTTP status always matches the code. The message is a human-readable string scoped to the partner contract — internal LMS vocabulary (Client, globalisation codes like error.msg.*) is rewritten or stripped before it reaches the wire, and it may change, so don’t parse it. code is an error-only field: successful responses return the resource object (no code), and 204 No Content has no body. Some errors also carry an optional sub_code — a stable, finer-grained business reason within a code. It lets you map an error to a specific customer-facing message without parsing message (see Sub-codes below). sub_code is additive and optional: an error may omit it, and new sub-codes may be introduced over time — so always branch on code first, and treat an unknown or absent sub_code by falling back to the code. For validation failures (invalid_argument), the message is a JSON-encoded array of {field, error} pairs:

Error codes

Sub-codes

When present, sub_code pins down the exact business scenario behind the code. Each maps cleanly to a fixed customer-facing message — only validation_error requires reading message (for the per-field detail). Branch on code first; use sub_code to refine the message you show.

Idempotency (repayments)

Repayments accept an optional idempotencyKey (your wallet transaction reference). Re-sending the same key on the same loan returns the original transaction with 200 — a safe replay, not an error. Re-using a key already recorded elsewhere (a different loan, or a race) returns already_exists (409).

Retrying

Example: branching on errors