PagBank
PagSeguro's Orders API v4 — Pix, card and boleto in integer centavos, one order id to reconcile on, and a webhook check that is not an HMAC.
PagBank (PagSeguro) through the Orders API v4 (api.pagseguro.com/orders): Pix, credit
and debit card, and boleto, authenticated with a Bearer token.
- Methods: pix, credit_card, debit_card, boleto. No
undefined— the Orders API makes you choose up front, so there is no "let the payer decide" order to create. - Setup:
payments.pagbank({ token, sandbox, notificationUrls })—PAGBANK_TOKEN; sandbox defaults toNODE_ENV !== 'production'(sandbox.api.pagseguro.com). - Money: integer centavos, end to end. PagBank's
amount.valueis the same unit this library uses, so nothing is divided or multiplied anywhere in the driver — R$ 19,90 is1990in your code and1990on the wire. externalReference: sent as the order'sreference_idand echoed on every webhook.- Idempotency:
idempotencyKeybecomes thex-idempotency-keyheader, which PagBank honours for 48 hours — a retried charge returns the original order instead of a second one.refund(id, amount, { idempotencyKey })sends it too. - Refunds:
POST /charges/{id}/cancel. Partial refunds leave the chargePAIDwith a non-zerorefundedsummary; a full one flips it toCANCELED. - Statuses:
AUTHORIZEDmaps toauthorized— PagBank's own word is "pré-autorizada": the money is held and nothing is captured untilPOST /charges/{id}/capture. It is notpending(which understates a hold the acquirer granted) and notpaid(which would grant access against money that evaporates). - No subscriptions, no customers, no invoice list, and no dispute event — see below.
Webhook verification is required at boot
This driver reports webhookVerification: 'unconfigured' — and the app refuses to boot — when
verifyWebhooks is off, which is what an unset webhookToken (and unset PAGBANK_WEBHOOK_TOKEN,
and no API token to fall back on) leaves you with.
Set it before you deploy, or — only when verification genuinely happens upstream — name this
provider in allowUnverifiedWebhooks. An empty credential slot is not "skip verification": with nothing to check against,
POST /payments/webhook/pagbank would accept any body anyone posted to it — including one
that marks a payment paid. The app refuses to start instead. See Configuration.
One order, two ids — and which one this driver uses
PagBank gives the same money two identifiers: the order (ORDE_…) and the charge inside it
(CHAR_…). Every payment is a charges[] entry — Pix too, since PagBank moved it into the
charge (payment_method.type: 'PIX', BR Code on charges[].qr_code.text); the driver still
reads the older order-level qr_codes shape when one turns up.
This driver uses the order id as gatewayId, always. The webhook delivers the order, so
keying on the charge id would file the charge you created and the webhook that confirms it
under two different ids, and nothing would reconcile. refund() accepts either id and
resolves the charge itself; findPayment() does too.
const payment = await payments.driver('pix').charge({
amount: 1990, // centavos, straight through
method: 'pix',
description: 'Plano Pro',
externalReference: 'pay:local_1',
customer: { name: 'Ana', email: 'ana@example.com', taxId: '123.456.789-09' },
})
payment.gatewayId // 'ORDE_…' — the id the webhook will carry
payment.pixCode // the BR Code ("copia e cola")The QR image is a link, not base64
PagBank returns links pointing at the rendered PNG (QRCODE.PNG, QRCODE.BASE64), not
the image bytes. pixQrCodeImage is documented as base64 content, so the driver leaves it
unset rather than filling it with a URL — read payment.payload.charges[0].links if you
want PagBank to render it for you, or generate the image yourself from pixCode.
Webhook authenticity — read this before you rely on it
PagBank sends an x-authenticity-token header with each Orders API notification. It is:
sha256( "<your API token>" + "-" + "<the exact raw body>" ) // hexThe driver verifies it, timing-safe, on every webhook, and rejects the request when it is missing or wrong. Three things about that mechanism deserve to be said plainly rather than implied:
- It is not an HMAC. It is a secret prefix hashed with SHA-256, which is the textbook
setup for a length-extension forgery: someone who has seen one valid
(body, token)pair can compute a valid token for that body plus a suffix without knowing the secret. What saves it here is that the forged body is the original followed by binary padding andJSON.parserejects that — the forgery has to get past the parser, not just the hash. It is a weaker construction than the HMAC every other gateway on these pages uses. - The secret is your API token. There is no separate webhook signing secret to configure or rotate. Anything that leaks your webhook secret has leaked your API credential, and rotating one rotates the other.
- The body must be byte-identical. Re-serializing the JSON before verifying breaks the hash. The lib-mounted route passes the raw body, so this only bites you if you verify it yourself somewhere else.
Set webhookToken when the account's webhook is configured with a token other than the API
one. verifyWebhooks: false exists for one reason — PagBank's sandbox does not always
send the header — and turning it off leaves the endpoint accepting anything that reaches it.
Turning it off is no longer silent: the driver then reports webhookVerification: 'unconfigured'
and the app refuses to boot unless you also name pagbank in allowUnverifiedWebhooks. Two
deliberate statements instead of one flag nobody re-reads.
providers: {
pagbank: payments.pagbank({
token: env.get('PAGBANK_TOKEN'),
notificationUrls: [`${env.get('APP_URL')}/payments/webhook/pagbank`],
}),
}No PagBank webhook maps to payment.disputed
The Orders API has no chargeback status and no chargeback event. A PagBank chargeback
arrives as a legacy post-transaction notification — form-encoded
notificationCode=…¬ificationType=transaction, delivered to the same
notification_urls, with no authenticity token at all — which you then resolve by
calling the v3 XML endpoint
(GET ws.pagseguro.uol.com.br/v3/transactions/notifications/{code}) with your account
email and token_api. The chargeback is the legacy transaction status 9, "Retenção
temporária" ("o comprador abriu uma solicitação de chargeback junto à operadora"); status
5, "Em disputa" is the earlier in-PagBank dispute.
That is a different API, a different format and different credentials, so this driver
rejects those bodies with a clear error rather than half-parsing them. If you take cards
through PagBank you need a second route for them; until then a chargeback here will not
move a payment row to disputed.
It follows that PagBank sends this driver no pre-dispute alert and no defense deadline
either: there is no fraud-notification event, no retrieval request, and nothing in the
legacy notification that names a date by which to respond. A contested PagBank sale is
answered in the PagBank panel. So none of payment.dispute_warning, payment.disputed or
payment.dispute_closed is ever emitted here — an honest gap, pinned by a test.
What it does not do
- Subscriptions. PagBank's recurring billing is the Assinaturas API — a different
product with a different base URL.
createSubscription,findSubscriptionand friends throw and name it, rather than returning something the gateway never created. - Customers. The Orders API has no customer resource; the payer travels inline on every
order, and
customer: { name, email, taxId }is required on every charge (PagBank refuses an order without a CPF/CNPJ).createCustomerthrows — keep the mapping between your users and their fiscal data in your own table. - Hosted checkout.
createCheckoutcreates a Pix charge and gives you a BR Code with an emptyurl; PagBank's redirect-style checkout is the separate Checkout API. - Invoice listing.
listInvoicesthrows. The Orders API has no invoice resource and no per-customer index —GET /ordersaccepts onlycharge_id, so you cannot recover a customer's history from PagBank at all and must keep the mapping yourself. It used to return[], which is indistinguishable from "this customer has no invoices" — something PagBank never told us. Fiscal notes come from an invoice provider.
Card and boleto
Card charges take the blob from PagBank's card-encryption SDK (the number never touches
your server), or a stored CARD_… id:
await payments.driver('credit_card').charge({
amount: 1990,
method: 'credit_card',
card: { token: encryptedCardFromFrontend, holder: { name: 'Ana', /* … */ } },
metadata: { installments: 3, capture: true },
})Boleto needs the payer's address, because PagBank prints it:
await payments.driver('boleto').charge({
amount: 1990,
method: 'boleto',
customer: { name: 'Ana', email: 'ana@example.com', taxId: '12345678909' },
metadata: {
dueDate: '2026-09-10',
address: {
street: 'Av. Brigadeiro Faria Lima', number: '1384', locality: 'Pinheiros',
city: 'São Paulo', region_code: 'SP', country: 'BRA', postal_code: '01452002',
},
},
})Pagar.me
Stone's Brazilian gateway on the v5 Core API — orders and charges over Pix, boleto and card, native subscriptions, and integer centavos end to end.
Efí
Efí's Pix API — the gateway that needs a client certificate before it will even issue you a token, and what that means for how you configure it.