Appearance
Error codes
Failed requests return an HTTP status between 400 and 599 and a JSON body with a code and a human-readable error and/or message:
json
{
"error": "amount should at least be 10.00",
"code": "unknown_error",
"message": "amount should at least be 10.00"
}Branch on the HTTP status first, then on code. Many request errors share the generic code unknown_error and are distinguished by HTTP status. The error / message text is for logs and humans and may change.
By HTTP status
| HTTP | Meaning |
|---|---|
| 400 | Malformed request, amount outside limits, or a signature field is malformed |
| 401 | Authentication failed — bad credentials, access token or signature (see below) |
| 403 | IP address not allowlisted, bank not supported, or refresh token already used |
| 404 | Refresh token not recognised |
| 409 | Duplicate invoice_no (deposits) |
| 422 | A payload field failed validation |
| 500 | Server error; for withdrawals also insufficient balance or duplicate invoice_no |
| 503 | Deposits or withdrawals are temporarily unavailable — retry later |
Codes
| Code | HTTP | Description |
|---|---|---|
unknown_error | 400, 409, 422, 500, 503 | Generic request error — see the HTTP status and message |
server_error | 500 | Server error |
FAILED | 401 | Access token missing, invalid, expired or superseded — get a new token |
credential.invalid_username_or_password | 401 | Wrong email or password |
credential.invalid_token | 404 | Refresh token not recognised |
credential.token_blocked | 403 | Refresh token already used or revoked |
Signature errors
Returned by signed endpoints when the request signature can't be verified. Body: {"error": "…", "code": "signature.…"}.
| HTTP | Code | What to check |
|---|---|---|
| 400 | signature.body.empty | Request body is empty |
| 400 | signature.body.invalid_json | Body must be JSON: {"data": "…"} |
| 400 | signature.data.missing | The data field is missing or empty |
| 400 | signature.data.invalid_base64 | data must be standard base64 (with padding) |
| 400 | signature.timestamp.missing | Send the X-Timestamp header |
| 400 | signature.timestamp.invalid | X-Timestamp must be Unix seconds as an integer |
| 401 | signature.timestamp.expired | Timestamp is > 60 s old — check your clock (NTP) |
| 401 | signature.timestamp.future | Timestamp is > 5 s in the future — check your clock (NTP) |
| 400 | signature.nonce.missing | Send the X-Nonce header |
| 400 | signature.nonce.invalid | Nonce must be 16–64 chars of [A-Za-z0-9_-] |
| 401 | signature.nonce.replayed | Nonce already used — generate a fresh one per request, including retries |
| 401 | signature.header.missing | Send the X-Signature header |
| 401 | signature.header.invalid_base64 | X-Signature must be standard base64 |
| 401 | signature.vendor.unresolved | Couldn't identify your account from the access token |
| 401 | signature.public_key.missing | We don't have your public key yet — contact your account manager |
| 401 | signature.verify.failed | Signing string mismatch or wrong key — see the checklist below |
| 500 | signature.nonce.store_failed | Temporary server error — retry with a new nonce |
Debugging signature.verify.failed
- URL — are you signing the exact URL you POST to, for the right environment? (rules)
- Data — is the signed
databyte-for-byte the same string you put in the body? Don't re-serialise between signing and sending. - Separators — five parts joined by single
.characters, no spaces or newlines. - Algorithm — RSA PKCS#1 v1.5 with SHA-256 (not PSS), signature standard-base64 encoded.
- Key — is the private key the pair of the public key you sent us, for this environment?
