Pay a Pix copy-and-paste code
Pay a Pix copy-and-paste code, static or dynamic, with the account balance.
A Pix copy-and-paste payment works like a withdrawal: same response, same tracking and the same limits, with its own fee. The destination comes from the code, and so does the amount, when the code sets one.
Decode the Pix copy-and-paste code
Optional. POST /transactions/pix/decode, scope PIX_DICT_READ, reads the code without paying and without querying the bank: it does not count toward the lookup limit and does not say who the holder is.
curl -X POST https://api.hub.payzu.com.br/api/v1/transactions/pix/decode \
-H "Authorization: Bearer $PAYZU_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "brCode": "00020126400014br.gov.bcb.pix0118fulano@exemplo.com520400005303986540525.005802BR5913FULANO DE TAL6009SAO PAULO62140510PEDIDO482163048572" }'const res = await fetch('https://api.hub.payzu.com.br/api/v1/transactions/pix/decode', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYZU_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
brCode: '00020126400014br.gov.bcb.pix0118fulano@exemplo.com520400005303986540525.005802BR5913FULANO DE TAL6009SAO PAULO62140510PEDIDO482163048572',
}),
});
const codigo = await res.json();{
"pixKey": "fulano@exemplo.com",
"url": null,
"amount": 2500,
"merchantName": "FULANO DE TAL",
"merchantCity": "SAO PAULO",
"txid": "PEDIDO4821",
"isDynamic": false,
"isAmountFixed": true
}isAmountFixed says whether the code already carries the amount. merchantName and merchantCity are what the code's creator wrote, without verification; to know the holder, use Look up recipient. In a dynamic code, pixKey comes null and only url is filled in; the holder and the amount come from the recipient lookup.
Pay the Pix copy-and-paste code
POST /transactions/pix/qr-payments, scope WITHDRAW. Send the full code in brCode, as it was read: a key decoded by your system is not accepted in its place. Send amount, in cents, only when the code does not set an amount.
Generate an Idempotency-Key for each payment and, if you repeat the call, send the same one.
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST https://api.hub.payzu.com.br/api/v1/transactions/pix/qr-payments \
-H "Authorization: Bearer $PAYZU_TOKEN" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"brCode": "00020126400014br.gov.bcb.pix0118fulano@exemplo.com520400005303986540525.005802BR5913FULANO DE TAL6009SAO PAULO62140510PEDIDO482163048572",
"comment": "Pedido 4821"
}'import crypto from 'node:crypto';
const idempotencyKey = crypto.randomUUID();
const res = await fetch('https://api.hub.payzu.com.br/api/v1/transactions/pix/qr-payments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYZU_TOKEN}`,
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
brCode: '00020126400014br.gov.bcb.pix0118fulano@exemplo.com520400005303986540525.005802BR5913FULANO DE TAL6009SAO PAULO62140510PEDIDO482163048572',
comment: 'Pedido 4821',
}),
});
const pagamento = await res.json();comment goes to the recipient; without it, the recipient name in the code goes instead. callbackUrl receives this payment's webhooks and requires the callback secret. All fields are in Pay Pix code.
Store the payment
The response (201) has the same format as a withdrawal. Store the id.
{
"id": "hubp-20261005H2P6XC8VNM127431",
"status": "APPROVED",
"amount": 2500,
"serviceFee": 100,
"totalDebited": 2600,
"pixKey": "f***@exemplo.com",
"comment": "Pedido 4821",
"e2e": null,
"providerRejectedReason": null,
"callbackUrl": null,
"createdAt": "2026-10-05T14:40:11.002Z",
"sentAt": "2026-10-05T14:40:11.380Z",
"approvedAt": "2026-10-05T14:40:11.702Z",
"confirmedAt": null
}pixKey comes masked. APPROVED means the payment is on its way, not that the money was delivered.
Confirm delivery
The payment is delivered when the WITHDRAW_COMPLETED webhook arrives, with status: "CONFIRMED" and operation: "EXTERNAL_PAYMENT". If it fails, WITHDRAW_FAILED arrives, and the amount and the fee come back to the balance.
To look it up, use the withdrawal route: GET /transactions/withdraw/{withdrawId}.
Amount
| The code | amount sent | Result |
|---|---|---|
| Sets an amount | None | Pays the code's amount. |
| Sets an amount | The same | Pays. |
| Sets an amount | A different one | 422 QR_AMOUNT_MISMATCH, with details.expected and details.requested. |
| Does not set one | An amount | Pays the amount sent. |
| Does not set one | None | 422 QR_AMOUNT_REQUIRED. |
Dynamic code
A dynamic code carries only a link, which PayZu resolves at the bank before paying; nothing leaves the balance before that. The body and the response are the same.
- If resolving fails, the rejection uses the recipient lookup codes:
404PIX_DEST_PIX_KEY,503PIX_DEST_UNAVAILABLEorPIX_DEST_THROTTLED,502PIX_DEST_NOT_AUTHORIZED_AT_PROVIDER. - If the amount printed in the code differs from the amount the bank returns for it, the payment is refused with
422QR_AMOUNT_DISAGREES.
Repeated request
The Idempotency-Key follows the withdrawal rules, and the comparison also considers the code being paid. For a dynamic code, the code is resolved before the repetition is checked, so repeating can bring the resolution rejections instead of the original payment.
On a 502, the payment may have gone out. Repeat with the same Idempotency-Key or look for the payment in GET /transactions/withdraw before paying again.
Fee and limits
- The fee is the Pix copy-and-paste payment fee,
externalPaymentin the limits. - The minimum and the maximum are the withdrawal ones. So are the daily cap and the request limit, counted together with withdrawals.
- A code that was already paid or has expired is refused by the bank.
- A corrupted code (
QR_CRC), a code out of format (QR_MALFORMED) or a code that is not Pix (QR_NOT_PIX) is refused with400, when decoding and when paying.
The rejections, with the code, are in Pay Pix code.