How to integrate the Pagnovo PIX API: a developer's guide
A practical guide to the Pagnovo API: authentication, creating a PIX charge, brCode, HMAC-signed webhooks, sandbox testing and the most common integration mistakes.
Pagnovo Team · 2026-08-04
This guide covers the full path of a PIX integration with Pagnovo — from your first authenticated call to the confirmation webhook — with code that actually works against our API.
Full, always-current reference: portal.pagnovo.com/docs.
First, how PIX works under the hood
PIX is the Central Bank of Brazil's instant payment system. The payer initiates the payment, their institution sends the order to the SPI, which validates, debits and credits — all in seconds, 24/7.
You do not connect to the Central Bank directly: only authorized institutions reach the SPI. Your application talks to Pagnovo, which exposes a REST API on top of it.
Step 1 — Authentication
Pagnovo uses secret key authentication through the Authorization header, in Basic format:
curl -X POST https://api.pagnovo.com/transactions/v2/purchase \
-H "Authorization: Basic $(echo -n 'secret:sk_test_YOUR_KEY' | base64)" \
-H "Content-Type: application/json" \
-d '{ "amount": 18990, "description": "Order #1042", "externalId": "order-1042" }'
Two important details:
- The environment comes from the key, not the URL.
sk_test_*runs in test;sk_live_*in production (after KYC approval). The base is alwayshttps://api.pagnovo.com. - Every key carries scopes. Reads require
view<Resource>; writes requiremanage<Resource>. Without the scope you get 403 "Unauthorized Permissions" — distinct from 401, which means invalid or missing credentials.
Optionally you can configure an IP allowlist for createWithdraw and createRefund. With it
active, calls from another IP return 400 "IP unauthorized".
Step 2 — Amounts in cents
This is the number one beginner mistake: all monetary values are integers in cents.
| Real amount | amount field |
|---|---|
| R$ 1.00 | 100 |
| R$ 189.90 | 18990 |
| R$ 1,500.00 | 150000 |
Percentage coupons and interest use basis points (1,000 = 10%); fixed penalties use cents.
Step 3 — Create the charge
POST /transactions/v2/purchase
{
"amount": 18990,
"description": "Order #1042",
"externalId": "order-1042",
"postbackUrl": "https://your-app.com/webhooks/pagnovo",
"restrictPayerDocument": true
}
Fields worth attention:
externalId— the order identifier in your system. Always send it: it lets you look the transaction up later without storing Pagnovo's ID.restrictPayerDocument— whentrue, only the given CPF/CNPJ can pay that QR. Excellent against third-party payment and useful for anti-fraud.postbackUrl— per-transaction webhook (V1 model). For production, prefer V2 webhooks registered once (step 5).
The response returns:
{
"id": "...",
"status": "PENDING",
"amount": 18990,
"brCode": "00020126...",
"qrCode": "..."
}
The brCode is the EMV copy-and-paste string; qrCode is the representation to display.
Step 4 — Show it to the customer
Offer both formats:
- Copy-and-paste with a copy button — essential on mobile, where the user is on the same device and cannot scan their own screen
- QR Code rendered from the
brCode
Forcing only the QR on mobile users is one of the biggest causes of abandonment.
Step 5 — Webhooks
Register the endpoint once and pick your events:
POST /v2/webhooks
{
"url": "https://your-app.com/webhooks/pagnovo",
"description": "Production",
"events": ["cashin.paid", "cashin.refunded", "cashout.success"]
}
The response returns a plain-text secret exactly once — store it safely. If you lose it, use
POST /v2/webhooks/:id/rotate-secret.
Available events include cashin.paid, cashin.refunded, cashout.success, cashout.failed,
cashout.returned, infraction.updated (replacing the old CHARGEBACK/BLOCKED) and the subscription
lifecycle (subscription.created, .activated, .paused, .canceled, .past_due, .expired).
The V2 envelope is standardized:
{
"event": "cashin.paid",
"environment": "TEST",
"payload": { "id": "...", "status": "APPROVED", "amount": 18990 }
}
Step 6 — Verify the signature (HMAC)
Never process a webhook without verifying the signature. An open endpoint is an invitation for someone to mark orders as paid without paying.
Pagnovo signs with HMAC-SHA256 over the payload with sorted keys:
import crypto from 'crypto';
function sortObjectKeys(obj: any): any {
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).sort().reduce((acc, key) => {
acc[key] = sortObjectKeys(obj[key]);
return acc;
}, {} as any);
}
return obj;
}
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(sortObjectKeys(payload)))
.digest('hex');
// Compare in constant time
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
The three classic causes of an invalid signature:
- Different re-serialization — whitespace or changed key order
- Unsafe comparison — using
===instead of a constant-time comparison - Wrong secret — mixing up the test and production keys
Step 7 — Reconcile
Webhooks are the primary path, but your application may be down when the call happens. Keep a fallback lookup for pending orders:
GET /transactions/:id
This endpoint accepts Pagnovo's ID, your externalId, or the PIX end2End — which is why
it is always worth sending externalId at creation.
Every response carries the x-trace-id header; keep it in your logs. That is what support uses
to find the exact request on our servers.
Step 8 — Test in the sandbox
Use an sk_test_* key. The sandbox has deterministic outcomes by amount, letting you exercise
every path without relying on luck:
| Amount | Outcome |
|---|---|
| R$ 10.00 | Approved |
| R$ 10.01 | Rejected |
| R$ 10.02 | Inconsistent |
| R$ 10.04 | Chargeback |
| R$ 10.05 | Refund |
| R$ 10.06 | Blocked |
The test environment is capped at 30 operations per day (transactions + withdrawals + refunds).
Most common mistakes
- Sending the amount in reais.
189.90becomes R$ 1.89. Always cents. - Confirming the order at creation. Charge created ≠ paid.
statusstarts asPENDING. - Skipping HMAC verification. Direct fraud exposure.
- Ignoring idempotency. The same webhook may arrive more than once — use the transaction
idas the key and respond 200 if already processed. - Responding slowly. Return 200 fast and process in a queue; slowness is treated as failure and triggers a retry.
- Confusing 401 with 403. 401 = invalid credentials. 403 = valid key, missing scope.
- Not storing
externalId. Without it, reconciling later gets much harder.
Pre-production checklist
-
sk_live_*key with the minimum required scopes - Amounts always in cents (test with
18990, not189.90) - V2 webhook registered, secret stored securely
- HMAC verification with constant-time comparison
- Idempotency keyed on the transaction
id - Immediate 200 response + asynchronous processing
- Fallback with
GET /transactions/:id -
x-trace-idrecorded in logs - Rejection and refund flows tested in the sandbox
Start with the official documentation, create your account at portal.pagnovo.com and explore our PIX API. Questions about your integration? Talk to our team.