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:
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 |
Branch on the code, not the sentence
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 |
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 and a QR 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.
There is no idempotency key
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, or the invoice, when you need to check whether a request that timed out actually landed.