Secure PIX cash-out: keys, QR Codes and IP allowlisting
How to implement outgoing payments with the Pagnovo API — cash-out by PIX key or QR, IP allowlist, scopes, receiver restriction and return events.
Pagnovo Team · 2026-08-08
Receiving money is relatively safe: worst case, the payment does not happen. Sending money is a different story — a mistake here means funds leaving your account to the wrong destination, often irreversibly.
This guide covers cash-out on the Pagnovo API with the right focus: security first.
Full reference: portal.pagnovo.com/docs.
Two ways to send
By PIX key (DICT)
POST /withdraws/cash-out
{
"amount": 50000,
"pixKey": "recipient@email.com",
"restrictReceiverDocument": true
}
By QR Code
POST /withdraws/cash-out/qrc
Use it when the recipient presents a QR (a supplier invoice, for example) instead of a key.
And to track it:
GET /withdraws/collect/:id → withdrawal status and lifecycle
The three layers of protection
What separates a safe cash-out integration from an accident waiting to happen:
1. IP allowlist
Pagnovo lets you restrict the most sensitive operations by IP — createWithdraw and
createRefund. With the list configured, a call from any other IP receives:
400 "IP unauthorized"
That means even if your key leaks, it cannot move money from outside your servers. It is the best cost-benefit protection in the whole integration — configure it before going to production.
An empty list means no restriction. Do not leave it that way in production.
2. Minimum scopes on the key
Every key carries scopes: reads require view<Resource>, writes require manage<Resource>. Without
the scope, the response is 403 "Unauthorized Permissions".
Take advantage of that: the key your checkout uses to create charges does not need to be able to withdraw. Separate keys by function. If the front-facing one leaks, the damage is bounded.
3. restrictReceiverDocument
{ "restrictReceiverDocument": true }
With this flag, the withdrawal only completes if the CPF/CNPJ of the key holder matches what you expect. It protects against the classic fraud scenario: someone swaps the registered PIX key and the money goes to a different person.
If you pay suppliers or make payouts, always use it.
The lifecycle and its events
Unlike cash-in, withdrawals have one extra state that needs handling:
| Event | Meaning | What to do |
|---|---|---|
cashout.success |
Withdrawal completed | Settle it in your system |
cashout.failed |
Failed | Investigate, notify, allow a retry |
cashout.returned |
Returned | Re-credit and investigate |
cashout.returned is the one integrations usually forget. The money left, was rejected by the
destination (closed account, invalid key, refund) and came back. If your system does not handle this
event, the balance returns but your record still says "paid" — and the difference only surfaces in
reconciliation, days later.
The balance you can actually withdraw
Before firing a withdrawal, check the balance — and read it carefully:
GET /accounts/balance
The response separates available, in settlement, collateral deposits, precautionary blocks and chargeback reserves. Only the available portion can be withdrawn.
Summing everything and trying to withdraw the total is a guaranteed failure — and worse, a promise to your user of a withdrawal that will not happen.
Idempotency: critical in cash-out
With charges, a duplicate webhook creates repeated work. With withdrawals, a duplicate request creates money sent twice.
Protect yourself before calling the API:
// Lock BEFORE firing
const lock = await db.withdrawLocks.insertIfAbsent({
key: `withdraw:${userId}:${requestId}`,
});
if (!lock) throw new Error('Withdrawal already requested');
const withdrawal = await pagnovo.post('/withdraws/cash-out', {
amount, pixKey, restrictReceiverDocument: true,
});
await db.withdraws.save({ ...withdrawal, traceId: response.headers['x-trace-id'] });
Practical rules:
- Lock per request, not per user
- Never auto-retry a withdrawal whose response you did not receive — query
GET /withdraws/collect/:idfirst and find out what happened - Store the
x-trace-idof every outgoing operation; it is what support uses to trace
Human approval for large amounts
Not everything should be automatic. A pattern that prevents serious losses:
amount <= automatic limit → process directly
amount > automatic limit → human approval queue
Add period limits (daily/monthly) and alerts for atypical patterns — many small withdrawals in sequence, a withdrawal right after a registered key change, a withdrawal to a key never used before.
Testing in the sandbox
With sk_test_* you can exercise the failure paths risk-free. Explicitly test:
- Successful withdrawal →
cashout.success - Failure →
cashout.failed - Return →
cashout.returned(the most forgotten one) - Call from an unauthorized IP →
400 - Key without withdrawal scope →
403
Remember the test environment allows 30 operations per day (transactions + withdrawals + refunds).
Checklist
- IP allowlist configured and not empty
- Withdrawal key separated from the charge key, with minimum scopes
-
restrictReceiverDocumentenabled on payouts - Idempotency lock before the call
-
cashout.returnedhandled with a re-credit - Balance read by category (available only)
- Amount threshold with human approval above the cap
-
x-trace-idstored for every operation - Failure and return paths tested in the sandbox
Explore our PIX API, see the withdrawals documentation and read our Fraud Prevention Policy. Talk to our team.