# 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._