Skip to content

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:

{
  "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:

FieldTypeWhat it is for
timestampintegerWhen the server rejected the request, in milliseconds since 1970
statusstringThe HTTP status and its name together, as in 400 BAD_REQUEST
codestringA short machine-readable reason. Branch on this
messagestringOne sentence about what went wrong. Written for a person
sub_errorsarrayOne 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

CodeWhat it meansWhat to do
400The request was malformed, or a value failed validationRead sub_errors. Fix the request; retrying it unchanged fails again
401No API key, or one the server does not recogniseCheck the Authorization header, and that the key matches the host
402The account cannot pay: no balance, or the payer’s bank declinedSurface it to the payer. This is theirs to fix, not yours
403The key is valid but not allowed to do thisUsually a key issued for a different currency account
404No such resource, or it belongs to another merchantCheck the id. A valid id from the other server also lands here
428A step is missing firstThe commonest cause is calling BurundiPay before onboarding
500Something broke on Leapa’s sideSafe 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.

View as markdown
Last updated