1. Guides
Pix2DePix API
  • ⚡ Quickstart
  • Guides
    • 🔑 Authentication
    • 🔄 Possible Statuses
    • 🔀 Synchronous requests & safe retries
    • ⚠️ Errors & the response envelope
    • 🧩 Troubleshooting
    • ✅ Best Practices
    • 🚀 Features
      • ⏱️ QR Delay
    • 🛡️ Security & Limits
      • 🚦 API Limits
      • 🧱 Firewall
      • 🪲 Bug Bounty
  • API Endpoints
    • Ping
      GET
    • Deposit (PIX ➔ DePix)
      POST
    • Deposit Status
      GET
    • Deposits
      GET
    • User Info
      GET
    • Withdraw
      POST
    • Withdraw Status
      GET
  • Webhooks
    • 🪝 Webhooks
    • Deposit Webhook
    • Withdraw Webhook
    • MED Webhook
  • Reference
    • 📖 Glossary
    • 📝 Changelog
  • Schemas
    • JWTClaims
    • ErrorObj
    • PingObj
    • ErrorResponse
    • DepositObj
    • PingResponse
    • DepositResponse
    • DepositStatusObj
    • DepositStatusResponse
    • DepositWebhookBody
    • DepositsResponse
    • DepositStatus
    • UserInfoResponse
    • WithdrawStatusResponse
    • WithdrawStatusObj
    • WithdrawResponse
    • WithdrawObj
    • WithdrawStatus
    • WithdrawWebhookBody
    • MEDWebhookBody
    • RejectionReasons
  1. Guides

⚠️ Errors & the response envelope

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.

The envelope#

There are three shapes you will receive.

Synchronous success#

A 2xx response wraps the result object under response, with async set to false.
{
  "response": { "...": "endpoint-specific fields" },
  "async": false
}

Synchronous error#

A non-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
}
When POST /deposit or POST /withdraw refuses a request on our side, the same envelope also carries a denialCode — see Denial codes.

Rejected before the operation#

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.

The authentication endpoints speak OAuth#

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.
errorStatusMeaning
invalid_request400Malformed body, an unrecognised field, a wrong JSON type, or an oversized body
invalid_client401Login failed — unknown, revoked, or wrong secret, deliberately indistinguishable
invalid_grant401Refresh failed — unknown, expired, revoked, or already used
rate_limited429Too many attempts; honour Retry-After
temporarily_unavailable503Try again shortly
server_error500Contact 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.

HTTP status codes#

Branch your logic on the HTTP status code. It is the stable, machine-readable signal.
StatusMeaning for this API
200 OKSuccess. Result is under response.
400 Bad RequestMalformed 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 UnauthorizedMissing, malformed, or expired token — or a token that has been revoked. Validate with GET /ping. Body is flat.
403 ForbiddenToken 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 FoundThe looked-up record (deposit, withdrawal, or user) does not exist.
413 Payload Too LargeThe request body exceeds the accepted size limit.
422 Unprocessable EntityCompliance block. Our risk screening declined this payer or this operation. Definitive — see Compliance blocks below.
429 Too Many RequestsRate limit exceeded. Read the Retry-After header (seconds) and back off before retrying. Body is flat.
500 Server ErrorUnexpected error on our side. Retryable on read-only calls; on POST /deposit and POST /withdraw see Retrying safely first.
502 Bad GatewayWe received an invalid (non-JSON) response from an upstream system. Same retry caveat as 500.
503 Service UnavailableTemporarily unavailable, for one of two reasons. The Retry-After header tells them apart — see below.
520A 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.

Compliance blocks (422)#

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).

Example error response#

A 520 from POST /deposit with a non-positive amount:
{
  "response": { "errorMessage": "amountInCents must be greater than zero." },
  "async": false
}

Denial codes#

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
}
denialCodeWhat it meansWhere you can receive it
BLOCKED_USERThe end user — the payer on a deposit, the beneficiary on a withdrawal — is blocked.POST /deposit, POST /withdraw
BLOCKED_MERCHANTThe merchant you passed in merchantId is blocked.POST /deposit
COMPLIANCE_BLOCKEDOur compliance screening refused the request.POST /deposit, POST /withdraw
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.

errorMessage is not a machine code#

Branch on denialCode, never on errorMessage text
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.

Retrying safely#

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 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.
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.
Modified at 2026-08-24 18:27:40
Previous
🔀 Synchronous requests & safe retries
Next
🧩 Troubleshooting
Built with