Every Pix2Depix operation endpoint speaks the same response envelope, whether it succeeds or fails. The API is synchronous: the connection is held open until the result is ready, and the body comes back inline. Learn the envelope once and you can parse every call the same way.Two things answer outside the envelope, and both are described below: a request refused before it reaches the operation — an authentication failure, a rate limit — and the three authentication endpoints.
Debugging a specific symptom?
This page explains the shape of every error. If you have a concrete symptom in front of you — a 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.
A request stopped on the way in — before it reaches the operation — is answered with the message at the top level, with no response wrapper and no async field:
{"errorMessage":"rate limit exceeded, slow down"}
On /api this is the shape of 401, 403 and 429, and only those. Every other status, including 422 and 520, uses the envelope above.
Read errorMessage from both places
401 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.
There is no asynchronous response
The API is synchronous-only. 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
A 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.
Branch your logic on the HTTP status code. It is the stable, machine-readable signal.
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 Record Not Found
The looked-up record (deposit, withdrawal, or user) does not exist.
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.
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.
Two kinds of 503, and the header separates them
Retry-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.No 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.The freeze is lifted by hand, so the 300 seconds is a back-off interval, not a promise that it will be over by then.
520 is a rejection, not an outage
520 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.The consequence is the trap: most HTTP libraries retry 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.
A 422 means our compliance and anti-fraud screening declined the payer or the operation. You can receive it from POST /deposit and POST /withdraw.Three things to know:
It is not something you can fix in your integration, and it is not the payer abandoning the payment. It is a decision on our side about that payer.
The message may or may not say why. Some refusals come back with a short reason in 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.
The body carries a support reference number, and that number is the whole point. Log it and quote it when you contact support. Without it we cannot look the case up.
{"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}
The body of a 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.
Do not retry a 422
Like 520, 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.
A 503 can mean timeout
Because the gateway holds the connection open for a synchronous result, a slow upstream surfaces as 503 (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).
When 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.
The code does not tell you the HTTP status, and the status does not tell you the code
Each refusal sets its own status, so two different denialCodes can arrive with two different HTTP statuses: on /api, COMPLIANCE_BLOCKED comes with 422 and BLOCKED_USER with 520. Branch on the code.Read both fields defensively. They appear only on a refusal, so no 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.
response.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.Where you need to react differently to different refusals, use denialCode (see above). Where there is none, 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.
There is no idempotency key — retries can duplicate money
The API does not support idempotent retries. 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.The consequence on POST /deposit and POST /withdraw is direct: a blind retry after a timeout or a 5xxcan 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.
Every response includes an X-Request-ID, and the X-Nonce the server generated, both of which are worth logging and quoting when you contact support.