404 on every call, a webhook that never arrived, an operation created twice — Troubleshooting is organised by symptom and gets you to the cause faster.2xx response wraps the result object under response, with async set to false.{
"response": { "...": "endpoint-specific fields" },
"async": false
}2xx response carries a human-readable string under response.errorMessage, with async set to false.{
"response": { "errorMessage": "amountInCents must be greater than zero." },
"async": false
}messageCode — see Message codes. When POST /deposit or POST /withdraw refuses a request on our side, it also carries a denialCode — see Denial codes.response wrapper and no async field:{ "errorMessage": "rate limit exceeded, slow down" }/api this is the shape of 401, 403 and 429, and only those. Every other status, including 422 and 520, uses the envelope above.errorMessage from both places401 is the first error a new integration meets and 429 is the one it meets under load, so a parser that only reads response.errorMessage fails on both. Read errorMessage from the top level and from inside response, whichever is present, and you cover every response this API can send.async is always false, there is no 202 Accepted and no urlResponse to poll. Sending X-Async: true is rejected up front with 400 — see Synchronous requests & safe retries.POST /api/v2/auth/login, /refresh and /logout answer in the OAuth dialect rather than in the envelope above. There is no response wrapper and no async field:{
"error": "invalid_grant",
"errorMessage": "invalid or expired refresh token"
}error is a stable machine-readable code and is the field to branch on. errorMessage is free text, present so that error handling written against the envelope keeps finding a field by that name.error | Status | Meaning |
|---|---|---|
invalid_request | 400 | Malformed body, an unrecognised field, a wrong JSON type, or an oversized body |
invalid_client | 401 | Login failed — unknown, revoked, or wrong secret, deliberately indistinguishable |
invalid_grant | 401 | Refresh failed — unknown, expired, revoked, or already used |
rate_limited | 429 | Too many attempts; honour Retry-After |
temporarily_unavailable | 503 | Try again shortly |
server_error | 500 | Contact support with the time of the request |
415 means you sent a Content-Type other than application/json. It uses the same two fields, with error set to invalid_request. See Authentication for the full flow.| Status | Meaning for this API |
|---|---|
200 OK | Success. Result is under response. |
400 Bad Request | Malformed or invalid request (e.g. missing required field, value out of range). Also returned when the request is rejected — including X-Async: true, which is no longer supported. |
401 Unauthorized | Missing, malformed, or expired token — or a token that has been revoked. Validate with GET /ping. Body is flat. |
403 Forbidden | Token is valid but lacks the required scope (deposit, withdraw / withdrawal, user) for this operation, or the partner is not permitted to call it. Body is flat. |
404 Not Found | No route matches the path, as when the /api prefix is missing or a base URL that ends in / produces a double slash. See Troubleshooting. A record that does not exist answers 520, never this. |
413 Payload Too Large | The request body exceeds the accepted size limit. |
422 Unprocessable Entity | Compliance block. Our risk screening declined this payer or this operation. Definitive — see Compliance blocks below. |
429 Too Many Requests | Rate limit exceeded. Read the Retry-After header (seconds) and back off before retrying. Body is flat. |
500 Server Error | Unexpected error on our side. Retryable on read-only calls; on POST /deposit and POST /withdraw see Retrying safely first. |
502 Bad Gateway | We received an invalid (non-JSON) response from an upstream system. Same retry caveat as 500. |
503 Service Unavailable | Temporarily unavailable, for one of two reasons. The Retry-After header tells them apart — see below. |
520 | A business rejection: the request was well-formed and authenticated, but refused. The reason is in response.errorMessage, and response.messageCode, when present, names the condition — see Message codes. |
503, and the header separates themRetry-After: 300 present — maintenance freeze. The operation was switched off for maintenance and your request was refused before reaching processing. Nothing was created, so there is no outcome to check. Wait the 300 seconds and resend.Retry-After: timeout. The request reached processing and no result came back in time. On POST /deposit and POST /withdraw the operation may already exist: confirm with GET /deposit-status or GET /withdraw-status before resending. On read-only calls, just retry.520 is a rejection, not an outage520 is a non-standard code inherited from the legacy gateway, and it sits in the 5xx range purely by accident of history. It means your request was refused on its merits — the amount was outside the allowed range, the Pix key did not match the beneficiary, the balance was insufficient.5xx automatically. On POST /deposit and POST /withdraw that turns a single rejection into repeated attempts, and if the underlying condition clears in between, into a duplicated operation. Disable automatic retry on these two endpoints, and treat 520 as final.422)422 means our compliance and anti-fraud screening declined the payer or the operation. You can receive it from POST /deposit and POST /withdraw.denialMessage (see Denial codes); others are generic. We never disclose the granular criteria behind the decision, so there is nothing further to parse out of either one.{
"response": {
"errorMessage": "After a compliance review, we are unable to process deposits for this payer at this time. If you believe this decision was made in error, please contact our support team and provide the following reference number: a1b2c3d4"
},
"async": false
}422 also carries statusCode: 422, repeating the HTTP status of the same response. It is the only error that carries it, and it tells you nothing the status line does not — branch on the status or on denialCode.422520, a 422 is a definitive refusal, not a transient failure. Retrying will not change the outcome, and because there is no idempotency (see below) a retry can create a second operation. Surface the reference number instead.503 can mean timeout503 (timeout) rather than a deferred response. On a call that moves money, a timeout does not tell you whether the operation was created — check its status before you retry (see Retrying safely).520 from POST /deposit with a non-positive amount:{
"response": { "errorMessage": "amountInCents must be greater than zero." },
"async": false
}POST /deposit or POST /withdraw refuses a request on our side — as opposed to rejecting something in what you sent — the envelope carries up to two extra fields next to errorMessage:denialCode: a short, stable, machine-readable string. This is the field to branch on.denialMessage: a human-readable sentence about the refusal. It appears only when it says something errorMessage does not already say, so it is often absent. Show it and log it, but never match on its text.{
"response": {
"errorMessage": "After a compliance review, we are unable to process deposits for this payer at this time. If you believe this decision was made in error, please contact our support team and provide the following reference number: a1b2c3d4",
"denialCode": "COMPLIANCE_BLOCKED"
},
"async": false
}denialCode | What it means | Where you can receive it |
|---|---|---|
BLOCKED_USER | The end user — the payer on a deposit, the beneficiary on a withdrawal — is blocked. | POST /deposit, POST /withdraw |
BLOCKED_MERCHANT | The merchant you passed in merchantId is blocked. | POST /deposit |
COMPLIANCE_BLOCKED | Our compliance screening refused the request. | POST /deposit, POST /withdraw |
denialCodes can arrive with two different HTTP statuses: on /api, COMPLIANCE_BLOCKED comes with 422 and BLOCKED_USER with 520. Branch on the code.denialCode simply means an ordinary error — and a denialCode your code does not recognise means a refusal more specific than your integration knows about. Treat an unknown code as a plain refusal instead of failing on it.messageCode next to errorMessage whenever the condition has a stable name. It is the machine-readable name of the condition, and it is the field to branch on: the wording of errorMessage is free to change, the code is not. The set is append-only and values are never renamed.denialCode also exists, the two carry the same value (BLOCKED_USER, BLOCKED_MERCHANT): one concept, one name. The exception is COMPLIANCE_BLOCKED, which carries only denialCode.{
"response": {
"errorMessage": "'lq1abc' is not a Liquid, Arkade or Spark address. Check the address and try again.",
"messageCode": "DEPOSIT_ADDRESS_NETWORK_UNKNOWN"
},
"async": false
}messageCode | What it means | Where you can receive it |
|---|---|---|
DEPOSIT_ADDRESS_NETWORK_UNKNOWN | The payout or split address is not a Liquid, Arkade or Spark address. Fix the address. | POST /deposit |
INVALID_DEPOSIT_ADDRESS | The wallet of the address's network refused the payout address (checksum on Liquid, server binding on Arkade). | POST /deposit |
INVALID_SPLIT_ADDRESS | Same as above, for depixSplitAddress. | POST /deposit |
SPLIT_ADDRESS_NETWORK_MISMATCH | depixSplitAddress is on a different network from the payout address. Both must be on the same one. | POST /deposit |
NETWORK_WALLET_UNAVAILABLE | The wallet of the address's network is not payable right now. Transient: retry later. | POST /deposit |
SPLIT_ADDRESS_REQUIRED | splitFee was sent without depixSplitAddress. | POST /deposit |
SPLIT_FEE_REQUIRED | depixSplitAddress was sent without splitFee. | POST /deposit |
SPLIT_ADDRESS_CONFLICT | depixSplitAddress equals depixAddress. | POST /deposit |
SPLIT_PORTION_TOO_LARGE | The split does not fit once the deposit fee is charged on the remainder. Raise amountInCents or lower splitFee. | POST /deposit |
PERMISSION_NOT_ENABLED | The request asks for something your partner permissions do not cover (whitelist, for example). | POST /deposit |
MISSING_PARAMETER | A required parameter was not sent. The message names it. | POST /withdraw, GET /user-info, GET /deposits |
INVALID_PARAMETER | A parameter has the wrong type or an invalid value. The message names it. | POST /deposit, POST /withdraw, GET /user-info, GET /deposits |
CONFLICTING_PARAMETERS | Two parameters contradict each other: the tax number and the euid refer to different people, or a withdrawal sets both depositAmountInCents and payoutAmountInCents. | POST /deposit, POST /withdraw |
IDENTIFICATION_REQUIRED | This deposit cannot be created without identifying the payer: send endUserTaxNumber or euid. The same sentence also arrives with no messageCode at all, and when only smaller deposits may go unidentified it carries for amounts above <value> before its final full stop, so match the message as well as the code. | POST /deposit |
NO_BANKING_NODE_AVAILABLE | No banking node available to this partner or merchant can take this deposit. | POST /deposit |
END_USER_NOT_FOUND | The euid resolved to nobody. | POST /deposit, POST /withdraw, GET /user-info |
DEPOSIT_NOT_FOUND | No deposit with this id belongs to your account. | GET /deposit-status |
WITHDRAW_NOT_FOUND | No withdrawal with this id belongs to your account. | GET /withdraw-status |
DEPOSIT_DAILY_LIMIT_REACHED | The payer's daily deposit ceiling is reached; the message names the maximum accepted for this deposit right now. | POST /deposit |
END_USER_DEPOSIT_DAILY_LIMIT_REACHED | The end user's own daily allowance is reached. | POST /deposit |
BLOCKED_USER | Same as the denialCode. | POST /deposit, POST /withdraw |
BLOCKED_MERCHANT | Same as the denialCode. | POST /deposit |
INTERNAL_ERROR | A failure on our side, which you cannot act on. Quote X-Request-ID to support. | any |
messageCode your integration does not recognise as a plain refusal instead of failing on it. When a refusal has no messageCode, read errorMessage and branch on the HTTP status.messageCode or denialCode, never on errorMessage textresponse.errorMessage is a free-text, human-readable string intended for logs and debugging. It is not a stable, machine-readable code, and its wording can change at any time — never match on its contents to drive control flow.messageCode (see above), or denialCode for the three compliance and block conditions that carry one. Where there is neither, use the HTTP status code.429 is safe to retry once you have waited out its Retry-After, and so are 5xx responses (500, 502, 503) on read-only calls.X-Nonce is generated by the server and returned to you for tracing; a nonce you send on the request is not read and does not deduplicate anything. There is no other idempotency mechanism.POST /deposit and POST /withdraw is direct: a blind retry after a timeout or a 5xx can execute the operation twice. Before resending, confirm the outcome with GET /deposit-status or GET /withdraw-status, or wait for the webhook. Turning off your HTTP library's automatic retry on these two endpoints is the safest default, since most libraries retry 5xx on their own — and the V1 rejection code 520 falls in that range.X-Request-ID, and the X-Nonce the server generated, both of which are worth logging and quoting when you contact support.