Skip to content
UseQR
ESC

↑↓ MOVE↵ OPEN48 PLACES

Payments

The PIX CRC16 checksum, and why hand-edited codes always fail

PIX uses CRC16/CCITT-FALSE — polynomial 0x1021, initial value 0xFFFF, no reflection, no final XOR — computed over the entire payload including the literal 6304 prefix, and written as four uppercase hex digits in field 63. Any edit made without recomputing the checksum makes every banking app reject the code.

View as MarkdownPaste this page into any AI assistant — it is plain, portable Markdown.

The parameters

Field 63 of every BR Code is a CRC16 in the CCITT-FALSE configuration:

Parameter Value
Polynomial 0x1021
Initial value 0xFFFF
Input/output reflection None
Final XOR None
Output 4 uppercase hex digits

This is the same CRC family used across EMVCo merchant-presented codes — PIX inherited it from the EMVCo base spec rather than inventing it. See CRC16 for the general concept.

How it runs

The algorithm walks the payload one byte at a time. Each byte is XORed into the high byte of a 16-bit register, then the register is shifted left eight times; whenever the top bit is set before a shift, the register is XORed with 0x1021. After the last byte, the register value — printed as four uppercase hex digits — is the checksum. UseQR's implementation is about ten lines of TypeScript in the open-source core (crc16ccittFalse in packages/core/src/payloads/pix.ts), if you want a reference to test against.

The detail that catches implementers

The CRC is computed over the payload including the literal 6304 field header, as if the four checksum digits themselves were simply absent:

input  = 000201...6207 0503 *** 6304
crc    = CRC16(input)          → e.g. A13F
output = input + "A13F"

The single most common implementation bug is computing the CRC over everything before 6304. The result is a well-formed string with a checksum that never matches, and a code that no app in Brazil will accept. If your generated codes fail in every app, check this first.

Why a hand-edited PIX string always fails

People try to edit a working copia e cola string — swap the amount, change the name — without regenerating it. This cannot work. The checksum was computed over the old bytes; after any edit it no longer matches, and apps reject the whole string. For a random edit, the chance the old checksum happens to remain valid is 1 in 65,536.

That is by design. The CRC exists to catch corruption — a mangled character in a copied-and-pasted string, a misdecoded module in a damaged print. A payments payload that tolerated single-character changes would be a fraud vector, not a convenience.

Also note the interaction with field lengths: every TLV field carries a two-digit byte length, so a non-ASCII character (whose UTF-8 encoding is more than one byte) silently breaks the length counting, which changes the bytes, which breaks the CRC. Transliterate names first — São Paulo must become Sao Paulo.

Verifying a code

Three ways, in increasing rigour:

  1. Decode the code with /validate and confirm the payload parses.
  2. Regenerate the payload from the same inputs in the PIX builder and compare strings — same inputs must produce the identical string, checksum included.
  3. Call GET https://useqr.app/api/v1/verify?data=…, which renders the code and decodes it back, proving the printed artefact will scan.

And always, before printing: scan with a real banking app and check the name and amount it displays.

FAQ

What CRC does a PIX QR code use?

CRC16/CCITT-FALSE — polynomial 0x1021, initial value 0xFFFF, no input or output reflection, no final XOR — output as four uppercase hexadecimal digits in field 63, computed over the whole payload including the literal 6304 prefix.

Why is my generated PIX code rejected by every app?

Most often the CRC was computed over the payload excluding the 6304 field header, or a non-ASCII character broke the field-length counting. Both produce a checksum mismatch, and apps reject mismatched codes without a specific error.

Can I edit a PIX string to change the amount?

Not by editing text. Any change invalidates the CRC16 and the string will be rejected. Regenerate the payload with the new amount so the checksum is recomputed over the new bytes.

Does the CRC protect against fraud?

It protects against corruption, not substitution. A fraudster can generate a fully valid code pointing at their own key. The fraud defence is the payee name shown on the confirmation screen, not the checksum.

Try it — free, no signup