Adding PIX QR codes to your product
The decision that matters is static versus dynamic, and it is a reconciliation decision, not a technical one. Get it wrong and you spend a quarter matching payments by hand.
PIX is how Brazil pays. Adding it to an invoice or a checkout is mostly a matter of putting the right string in front of the payer — but there is one architectural decision underneath, and teams routinely discover it after launch, when someone asks "so how do we know which invoice this payment was for?"
Static or dynamic: decide this first
| Static BR Code | Dynamic BR Code | |
|---|---|---|
| What it encodes | Your PIX key, directly, plus optional amount | A URL to a payload hosted by your PSP |
| Who can create it | You, offline, in ~40 lines | Your bank or PSP, via their API |
| Reusable | Yes, indefinitely | Typically per-charge, with an expiry |
| Per-charge amount | Possible, but the code is still fungible | Yes, bound to the charge |
| Automatic reconciliation | No — see below | Yes — webhook identifies the charge |
| Cost | Zero | PSP pricing, account, integration |
| Good for | Invoices a human reconciles, donations, tip jars, small volume | Checkout, subscriptions, anything automated |
A static code with an amount and a txid looks like it supports reconciliation. It does not, reliably: the payer's bank does not guarantee your reference reaches your statement in a machine-readable form, the payer can edit the amount in many flows, and nothing stops the same code being paid twice. If your process is "customer pays, system marks invoice paid, automatically", you need dynamic codes and a PSP webhook. If your process is "the finance team looks at the statement", static is fine and free.
Practical middle ground used by a lot of small Brazilian businesses: static code on the invoice, with the invoice number in the description field, and a human matching the bank statement. It works, it costs nothing, and it stops working somewhere around a few dozen payments a month.
Generating a static code
The payload is EMV type-length-value ASCII — every field is documented in the BR Code format reference, with a real payload dissected byte by byte. The generator:
const tlv = (id, v) => id + String(String(v).length).padStart(2, "0") + v;
/** Uppercase, unaccented, [A-Z0-9 ] only. 1 char must equal 1 byte. */
const clean = (s, max) =>
String(s ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.toUpperCase().replace(/[^A-Z0-9 ]/g, "").trim().replace(/\s+/g, " ").slice(0, max);
function crc16(str) {
let crc = 0xffff;
for (let i = 0; i < str.length; i++) {
crc ^= str.charCodeAt(i) << 8;
for (let b = 0; b < 8; b++) {
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
}
}
return crc.toString(16).toUpperCase().padStart(4, "0");
}
export function buildPixCode({ key, name, city, amount, txid }) {
const account = tlv("00", "BR.GOV.BCB.PIX") + tlv("01", key);
const body =
tlv("00", "01") + tlv("01", "11") + tlv("26", account) +
tlv("52", "0000") + tlv("53", "986") +
(amount > 0 ? tlv("54", Number(amount).toFixed(2)) : "") +
tlv("58", "BR") + tlv("59", clean(name, 25)) + tlv("60", clean(city, 15)) +
tlv("62", tlv("05", clean(txid, 25) || "***")) + "6304";
return body + crc16(body);
}Render it with any QR library at error-correction level M. Or skip the code entirely while prototyping: the free PIX tool builds and displays one in your browser and shows the resulting string, which is a fast way to sanity-check your own implementation against a known-good output.
Putting it on the invoice
The pattern that works: total, amount in words, QR code, and the copy-paste string underneath — because a meaningful share of payers are on the same device and cannot scan their own screen.
const brCode = buildPixCode({
key: "contato@exemplo.com.br",
name: "Atlas Solucoes Digitais",
city: "Sao Paulo",
amount: 149.90,
txid: "PED0001",
});
const res = await fetch("https://brazilian-invoice-receipt-pdf-api-cpf-cnpj.p.rapidapi.com/invoice", {
method: "POST",
headers: {
"content-type": "application/json",
"X-RapidAPI-Key": process.env.RAPIDAPI_KEY,
"X-RapidAPI-Host": "brazilian-invoice-receipt-pdf-api-cpf-cnpj.p.rapidapi.com",
},
body: JSON.stringify({
numero: "PED0001",
emitente: { nome: "Atlas Solucoes Digitais LTDA", documento: "11222333000181" },
destinatario: { nome: "Comercio Silva & Filhos ME", documento: "22333444000181" },
itens: [{ descricao: "Licenca mensal", valor_unitario: 149.90 }],
forma_pagamento: "PIX",
pix_copia_cola: brCode, // rendered as a QR on the PDF
}),
});A 145-character payload produces a fairly dense QR. Keep it at least 3 cm square at 300 dpi with a clear quiet zone, and resist the urge to put a logo in the middle unless you have raised the error correction level to compensate. The most common "the QR does not scan" report is a printing problem, not a payload problem.
Scope: encoding is not authorization
It formats a string. It does not contact a bank, does not verify that the key belongs to you, does not create a charge, and does not move money. The payment happens when someone scans it and their bank sends funds to whoever owns that key.
Two consequences worth designing for: (1) a typo in the key sends money to a stranger with no recall mechanism — always echo the key back for confirmation before persisting it, and never auto-populate it from an unverified source; (2) because you are asserting who should be paid, never let one user supply the key that appears on another user's document.
This is also why FaturaPDF takes pix_copia_cola as an input rather than
offering to generate it: minting a PIX code implies a claim about who owns a key, and a third-party API
has no basis for making that claim on your behalf. Rendering a code you supply carries no such implication.
Reconciliation, if you are doing it manually
Static codes plus human matching is a legitimate stage of a business. Make it survivable:
- Put a short, unique reference in the txid field (
62.05) — the invoice number, uppercase alphanumeric, under 25 characters. Some flows surface it; when they do, it saves minutes. - Also put it in the description (
26.02), which some apps show the payer as they confirm. Payers who type it into the transfer note make your life much easier. - Set the amount when you know it. Matching by amount plus date is what actually works in practice, and distinct amounts (R$ 149,90 rather than R$ 150,00) collide far less often.
- Track expected payments in your own system so the match is a query rather than a search.
When this stops scaling — and it will — the migration is to a PSP with dynamic codes and a webhook. Design for it now by keeping the BR Code string in a column rather than generating it inline at render time: swapping "build it locally" for "fetch it from the PSP" then touches one function.
Common mistakes
| Mistake | Consequence |
|---|---|
| Accented characters in name or city | Byte length ≠ declared length; the payload misaligns and stricter apps reject it outright |
Amount as "149,90" | Rejected. Field 54 is dot-decimal, no separator, no symbol |
| CRC over the wrong span | Universally rejected. It must include the trailing "6304" |
| Reusing one static code for every customer | Reconciliation becomes guesswork the moment two people owe similar amounts |
| Assuming txid comes back on your statement | It often does not. Do not build automation on it |
| Storing the QR image instead of the string | You cannot re-render at another size, verify the CRC, or diff it |
Frequently asked questions
Can I generate a PIX QR code without a bank integration?
Yes, for static codes: the payload only encodes your own PIX key, so it is pure string formatting — around 40 lines, offline, free. Dynamic codes host their payload at your PSP and do require integration.
Does the API generate PIX codes for me?
No, deliberately. FaturaPDF renders a BR Code you supply in pix_copia_cola onto the PDF. Minting a code implies asserting who owns a PIX key, which is not something a third-party API should do on your behalf — and you can build a static code yourself with the snippet above.
How do I know which invoice a PIX payment was for?
Reliably, only with a dynamic code and a PSP webhook that identifies the charge. With static codes, teams match on amount plus date plus an expected-payments list, and put the invoice number in the txid and description as a hint. That works at low volume and stops working as you grow.
What is the maximum length of a PIX payload?
There is no hard container limit, but keep it short — every character adds QR modules and hurts scannability in print. Around 150 characters is typical; the description field is the usual source of bloat.
Putting the QR on the document
FaturaPDF takes a BR Code string you already have and renders it as a scannable QR next to the total on the invoice or receipt. It never mints PIX codes — see the scope note below for why that is deliberate.
Get an API key on RapidAPI → Or try the free browser generator