Reference

The PIX BR Code format, field by field

A PIX "Copia e Cola" string is not opaque — it is EMV type-length-value, readable with your eyes, ending in a CRC-16. Here is every field, with a real payload dissected.

Updated 2026-08-07 · 10 min read · by the FaturaPDF team

PIX is Brazil's instant payment system, and the QR code you scan to pay is not a URL or an opaque token. It is a plain ASCII string in EMV® QR Code format — the same type-length-value encoding used by merchant QR standards worldwide, with a Brazilian Central Bank (Banco Central do Brasil) profile on top. The same string is what people paste as "PIX Copia e Cola".

Because it is TLV, you can read one by hand once you know the rules. This page walks a real payload byte by byte, lists every field that matters, and gives you working code. If what you actually need is the product decision — static code or dynamic code, and how reconciliation works — start with adding PIX to your product instead.

TLV in thirty seconds

The payload is a flat concatenation of records. Each record is:

textrecord layout
+----+--------+-----------------+
| ID | LENGTH |      VALUE      |
+----+--------+-----------------+
  2       2       LENGTH chars

ID     = two ASCII digits, the field number
LENGTH = two ASCII digits, the value's length in CHARACTERS, zero-padded
VALUE  = exactly LENGTH characters

Records simply follow one another with no separator: read two characters for the ID, two for the length, then that many for the value, then repeat. Some fields are templates whose value is itself a sequence of TLV records — nesting, one level deep in practice.

LENGTH counts characters, and that means bytes

The encoding is effectively ASCII. An accented character in UTF-8 occupies two bytes, so a naïve implementation that writes "JOÃO" computes length 4 while emitting 5 bytes — and every subsequent field is misaligned, producing a code that no bank app can read. Strip accents and restrict to [A-Z0-9 ] before measuring. This is the single most common bug in home-grown PIX generators.

A real payload, dissected

Here is a static PIX code for an e-mail key, receiver "Atlas Solucoes" in "Sao Paulo", amount R$ 149,90, transaction id PED0001:

text145 characters, no line break in the real string
00020101021126440014BR.GOV.BCB.PIX0122contato@exemplo.com.br52040000530398654061
49.905802BR5914ATLAS SOLUCOES6009SAO PAULO62110507PED00016304D346

Parsed:

textfield-by-field
00 len=02  "01"                     Payload Format Indicator (always "01")
01 len=02  "11"                     Point of Initiation: 11 = static/reusable
                                                         12 = dynamic/single-use
26 len=44  Merchant Account Information - PIX (a template)
   |
   +-- 00 len=14  "BR.GOV.BCB.PIX"  Globally Unique Identifier - always this
   +-- 01 len=22  "contato@exemplo.com.br"   the PIX key itself

52 len=04  "0000"                   Merchant Category Code (0000 = unspecified)
53 len=03  "986"                    Currency, ISO 4217 numeric: 986 = BRL
54 len=06  "149.90"                 Amount. DOT decimal, no thousands separator.
                                    OMIT this field to let the payer type it.
58 len=02  "BR"                     Country Code
59 len=14  "ATLAS SOLUCOES"         Receiver name   (max 25 chars)
60 len=09  "SAO PAULO"              Receiver city   (max 15 chars)
62 len=11  Additional Data Field (a template)
   |
   +-- 05 len=07  "PED0001"         Reference label / txid

63 len=04  "D346"                   CRC-16 over everything before it, incl. "6304"

Drop the amount and the txid and the same receiver produces a shorter, open-value code:

textno fixed amount
00020101021126440014BR.GOV.BCB.PIX0122contato@exemplo.com.br5204000053039865802BR
5914ATLAS SOLUCOES6009SAO PAULO62070503***6304E652

Two changes worth noticing: field 54 is gone entirely (the payer will enter the amount), and field 62 now carries 05 = "***", the conventional placeholder for "no specific transaction identifier". The CRC changes too, of course — it always does.

The field reference

IDFieldRequiredNotes
00Payload Format IndicatoryesAlways "01". Must be first.
01Point of Initiation Methodno"11" static (reusable), "12" dynamic (single use). Omitted implies static.
26Merchant Account InformationyesTemplate. Holds the GUI, the key, and an optional description.
26.00GUIyesAlways "BR.GOV.BCB.PIX".
26.01PIX keyyes*CPF/CNPJ (digits only), phone as +5511999999999, e-mail, or a 36-char random key. Max 77.
26.02DescriptionnoFree text shown to the payer. Counts against the total payload length.
52Merchant Category Codeyes"0000" unless you have a real ISO 18245 MCC.
53Transaction Currencyyes"986" (BRL).
54Transaction Amountno"149.90" — dot decimal, no separators, no symbol. Omit for open value.
58Country Codeyes"BR".
59Merchant NameyesMax 25 chars. Uppercase, unaccented.
60Merchant CityyesMax 15 chars. Uppercase, unaccented.
62Additional Data FieldnoTemplate; in practice carries 05.
62.05Reference Label (txid)noMax 25 chars, [A-Za-z0-9]. "***" means none.
63CRC-16yesAlways last, always length 04, uppercase hex.

* A static code identifies the receiver by key. Dynamic codes instead put a URL in 26.25 pointing at a payload hosted by the receiver's PSP — that flow requires bank integration and is out of scope here.

The CRC-16, precisely

Field 63 is a CRC-16/CCITT-FALSE: polynomial 0x1021, initial value 0xFFFF, no input or output reflection, final XOR 0x0000, rendered as four uppercase hex characters.

The subtle part: the checksum is computed over the entire payload including the literal "6304" that opens the CRC field itself, but excluding the four characters of the CRC value. So you build the whole string, append "6304", hash that, and then append the result.

javascriptcrc16.js
function crc16(str) {
  let crc = 0xffff;
  for (let i = 0; i < str.length; i++) {
    crc ^= str.charCodeAt(i) << 8;
    for (let bit = 0; bit < 8; bit++) {
      crc = (crc & 0x8000) !== 0 ? ((crc << 1) ^ 0x1021) & 0xffff
                                 : (crc << 1) & 0xffff;
    }
  }
  return crc.toString(16).toUpperCase().padStart(4, "0");
}

// Verify any code you receive:
export function isValidBrCode(payload) {
  if (payload.length < 8) return false;
  const body = payload.slice(0, -4);              // everything incl. "6304"
  const given = payload.slice(-4).toUpperCase();
  return body.endsWith("6304") && crc16(body) === given;
}

Round-tripping the example above: the body ends ...62110507PED00016304 and hashing it yields D346, which is what the payload carries. If a bank app says "invalid code", check the CRC first — it is wrong far more often than the field layout is.

Building one, end to end

javascriptpix.js
const tlv = (id, value) => {
  const v = String(value);
  return id + String(v.length).padStart(2, "0") + v;
};

/** Uppercase, unaccented, [A-Z0-9 ] only, truncated. 1 char = 1 byte. */
const clean = (input, maxLen) =>
  String(input ?? "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")   // drop combining accent marks
    .toUpperCase()
    .replace(/[^A-Z0-9 ]/g, "")
    .trim()
    .replace(/\s+/g, " ")
    .slice(0, maxLen);

/** CPF/CNPJ keys must be digits only; other key types pass through. */
function normalizeKey(raw) {
  const s = String(raw ?? "").trim();
  const d = s.replace(/\D/g, "");
  return (d.length === 11 || d.length === 14) && /^[\d.\-/]+$/.test(s) ? d : s;
}

export function buildPixPayload({ key, name, city, amount, description, txid }) {
  const account =
    tlv("00", "BR.GOV.BCB.PIX") +
    tlv("01", normalizeKey(key)) +
    (description ? tlv("02", clean(description, 72)) : "");

  const body =
    tlv("00", "01") +                                   // format indicator
    tlv("01", "11") +                                   // static
    tlv("26", account) +
    tlv("52", "0000") +                                 // MCC
    tlv("53", "986") +                                  // BRL
    (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";                                             // opens field 63

  return body + crc16(body);
}

Turn the string into an image with any QR library — qrcode in JS, qrcode in Python, go-qrcode in Go. Error correction level M is the usual choice: level H bloats the module count for a 145-character payload with no practical benefit on a screen or a printed invoice.

Try it without writing anything: the free PIX tool builds and renders a BR Code entirely in your browser, and shows the resulting string so you can compare it against your own implementation.

Debugging a code that will not scan

SymptomAlmost always
"Invalid QR code" in every bank appCRC computed over the wrong span — you forgot to include the trailing "6304", or you hashed the full string including the old CRC.
Parses in one app, fails in anotherA length that does not match its value. Usually an accent, or a name longer than 25 characters that you truncated after computing the length.
Receiver name shows as garbageNon-ASCII in field 59. Strip to [A-Z0-9 ].
Amount rejected or wrongComma decimal ("149,90"), a currency symbol, or a thousands separator in field 54. It must be "149.90".
Works on screen, fails on paperPrint resolution, not payload. Keep the QR at least 3 cm square at 300 dpi with a clear quiet zone.

A useful habit: write a parser before you write a generator. Twenty lines that walk the TLV and print each field will tell you in one glance where the alignment broke.

javascriptparse.js
export function parseBrCode(payload, depth = 0) {
  const out = [];
  for (let i = 0; i < payload.length; ) {
    const id = payload.slice(i, i + 2);
    const len = parseInt(payload.slice(i + 2, i + 4), 10);
    if (Number.isNaN(len)) throw new Error(`bad length at offset ${i}`);
    const value = payload.slice(i + 4, i + 4 + len);
    if (value.length !== len) throw new Error(`truncated field ${id} at offset ${i}`);
    out.push({ id, len, value,
      children: (id === "26" || id === "62") && depth === 0
        ? parseBrCode(value, depth + 1) : undefined });
    i += 4 + len;
  }
  return out;
}

Scope and safety

Encoding is not authorization

Building a BR Code is pure string formatting. It does not contact any bank, does not verify that the key belongs to you, and does not create a charge — it encodes what you typed, the way a vCard library encodes a contact. The payment happens when a payer scans it and their bank moves money to whoever owns that key. Two consequences: (1) a typo in the key sends money to a stranger, so echo the key back for confirmation in your UI; (2) if you need a code that a PSP will reconcile automatically against a specific charge, you want a dynamic code issued through your bank's API, not a static one.

The same applies to FaturaPDF's pix_copia_cola field: the API renders the QR you supply onto the PDF. It never mints PIX codes, which is deliberate — minting one implies a claim about who owns the key, and an API has no business making that claim on your behalf.

Frequently asked questions

Is the PIX BR Code the same as a generic EMV QR code?

It uses the same EMV type-length-value container, with a Brazilian Central Bank profile: the merchant account template (field 26) must carry the GUI BR.GOV.BCB.PIX plus the PIX key. A generic EMV parser will read the structure fine but will not understand the payment semantics.

What is the difference between a static and a dynamic PIX code?

A static code (01 = 11) embeds the key directly and can be reused indefinitely — it is what this page builds. A dynamic code puts a PSP-hosted URL in the merchant template; the payer's bank fetches the real payload, which enables per-charge amounts and automatic reconciliation. Dynamic codes require integration with your bank or PSP.

Why does my QR code work in one bank app but not another?

Nearly always a length/value mismatch caused by a non-ASCII character, or a CRC computed over the wrong span. Stricter parsers reject it, lenient ones resynchronize. Run the payload through a TLV parser and check that every declared length matches its value exactly.

Can I leave the amount out?

Yes — omit field 54 entirely and the payer types the amount in their bank app. Do not send "0.00"; that is a zero-amount charge, not an open one.

How long can the payload be?

There is no hard limit in the container, but keep it short: every character adds QR modules and hurts scannability, especially in print. Around 150 characters is typical for a static code with a name, city and txid. The description field (26.02) is the usual cause of bloat.

Put the QR on an invoice

Once you have a BR Code string, pass it as pix_copia_cola and FaturaPDF renders it as a scannable QR on the invoice or receipt PDF, next to the total.

Get an API key on RapidAPI → Or try the free browser generator

Related guides