# Errors One error shape, the status codes that carry it, and what to do about each. Anything from `400` up returns the same JSON object, whatever endpoint produced it: ```json { "timestamp": 1769850251000, "status": "400 BAD_REQUEST", "code": "VALIDATION_FAILED", "message": "amount must be greater than zero", "sub_errors": [ { "message": "amount: must be greater than zero" } ] } ``` Five fields, and each has one job: | Field | Type | What it is for | | --- | --- | --- | | `timestamp` | integer | When the server rejected the request, in milliseconds since 1970 | | `status` | string | The HTTP status and its name together, as in `400 BAD_REQUEST` | | `code` | string | A short machine-readable reason. Branch on this | | `message` | string | One sentence about what went wrong. Written for a person | | `sub_errors` | array | One entry per field that failed validation, when the failure was a validation one | `message` is prose. Its wording can change without the API version changing, and it is not a contract. Read the HTTP status for the category and `code` for the specific reason; use `message` and `sub_errors` for what you log and what you show. ## Status codes | Code | What it means | What to do | | --- | --- | --- | | `400` | The request was malformed, or a value failed validation | Read `sub_errors`. Fix the request; retrying it unchanged fails again | | `401` | No API key, or one the server does not recognise | Check the `Authorization` header, and that the key matches the host | | `402` | The account cannot pay: no balance, or the payer’s bank declined | Surface it to the payer. This is theirs to fix, not yours | | `403` | The key is valid but not allowed to do this | Usually a key issued for a different currency account | | `404` | No such resource, or it belongs to another merchant | Check the id. A valid id from the other server also lands here | | `428` | A step is missing first | The commonest cause is calling BurundiPay before [onboarding](/docs/api/burundipay/onboard) | | `500` | Something broke on Leapa’s side | Safe to retry once, after a delay. If it persists, contact support | Every endpoint in this reference can return all seven. The status-code list at the foot of each endpoint page repeats them so you never have to come back here to check. ## Retrying safely `400`, `401`, `403`, `404` and `428` are answers about your request. Retrying without changing anything gets the same answer, so do not. `500` and a network timeout are different, and the difference matters: the request may have been received and acted on before the connection dropped. Re-sending a charge or a payout can create a second one. Read the state back before you decide: - An invoice being paid over BurundiPay has [an RTP status](/docs/api/burundipay/payment-request-status) and [a QR status](/docs/api/burundipay/qr-code-status). Both reconcile with the central bank at the moment you read them, so they answer “did it go through?” directly. - Anything else: fetch the resource by its id, or list the collection, and only create it again if it is genuinely absent. This version of the API has no header that makes a repeated `POST` safe. Two identical create requests make two resources. Record the id the first response gave you before you retry anything. Customers and invoices come back carrying a `reference_id` that Leapa assigns. Store it against your own order, and [look the customer up by it](/docs/api/customers/retrieve-by-reference), or [the invoice](/docs/api/invoices/retrieve-by-reference), when you need to check whether a request that timed out actually landed. --- _Generated from the Leapa documentation at leapa.co/docs._