Montei um gerador de "Pix copia e cola" seguindo o Manual do BR Code do Banco Central e alguns repositórios de referência. Joguei o payload em três validadores online: todos aprovaram. CRC batendo, campos no lugar, estrutura TLV correta.
Aí abri o app do banco pra ler o QR de verdade: "QR Code inválido".
Este post é sobre as duas coisas que saíram daí:
- O formato do Pix copia e cola é público e cabe em ~120 linhas de C#, sem gateway e sem biblioteca (só uma de QR pra virar imagem).
-
As três coisas que passam em todo validador genérico e mesmo assim o banco recusa: a caixa da GUI
br.gov.bcb.pix, o formato da chave no DICT e o campo01. A próxima seção resolve cada uma; o resto do post é como cheguei lá.
E fica a suspeita que me trouxe até aqui: se o meu passou em três validadores e mesmo assim quebrou num banco de verdade, quantos QR Pix "quase válidos" estão circulando por aí agora?
As três armadilhas, direto ao ponto
Se você caiu aqui procurando "QR Code Pix inválido" e tem um payload que parece certo, é quase certeza que é uma destas três. O detalhamento, com código e fontes, está mais abaixo; isto aqui já desbloqueia:
-
A caixa da GUI. O identificador do arranjo,
br.gov.bcb.pix, aparece em minúsculo nos manuais do Banco Central, mas o app do meu banco só aceitava em maiúsculo,BR.GOV.BCB.PIX. O checksum fecha dos dois jeitos, então nenhum validador acusa. Regra prática: gere um copia e cola pelo app do próprio banco de quem vai receber e use exatamente a caixa que ele usa. -
O formato da chave. O banco de quem paga busca a chave no DICT (o diretório de chaves do BC). Se ela não estiver no formato exato (telefone como
+5531999998888, e-mail em minúsculo, CPF só com os 11 dígitos), ele não encontra e recusa, mesmo com o BR Code impecável no resto. -
O campo
01(Point of Initiation Method). Num QR estático, declarar12(o valor de QR dinâmico) quebra;11funciona e omitir também. Nunca mande12num QR reutilizável.
O resto do post: o que é o BR Code, como montar o payload do zero e como testar sem ficar abrindo o app do banco a cada mudança.
O contexto
Eu estava construindo um bot de Telegram que divide as contas de um grupo (viagem, república, happy hour) e no fim calcula quem transfere quanto pra quem. Faltava a última peça: gerar o Pix de cada transferência ali no chat. Integrar um gateway (Mercado Pago, Asaas, Toku, Gerencianet) pede conta, credencial, OAuth, webhook público e normalmente uma taxa por transação. É desproporcional pra gerar um QR estático de "me paga R$ 42,50". E o "Pix copia e cola" que o app do banco me dá pra receber é só uma string: 00020126...6304XXXX. Se é só texto, dá pra gerar.
("Pix copia e cola", BR Code e QR Code Pix são o mesmo objeto: um texto no formato EMV® MPM (Merchant-Presented Mode) do EMVCo, que o Banco Central adaptou. A diferença é só se você mostra a string ou a imagem; a imagem é a mesma string codificada num QR.)
O código completo está em RachaContaBot (
RachaContas.Bot/Services/PixBrCodeGenerator.cs). É C# / .NET 10, mas o formato não tem nada de específico de linguagem.
(Testei os payloads gerados nos apps de Banco do Brasil, Bradesco, Itaú, Nubank e Inter em 19 de junho de 2026, e os cinco leram e liquidaram. Comportamento de PSP muda sem aviso; se você está lendo isto bem depois e algo não bater, o método de teste no fim do post é o que vale.)
O que é um "Pix copia e cola"
Em uma frase: é uma string no padrão BR Code (a adaptação brasileira do EMV® MPM, Merchant-Presented Mode: o comerciante apresenta o código, o cliente escaneia) que carrega, em texto, todos os dados de uma cobrança: chave, valor, nome e cidade de quem recebe, um identificador e um checksum. Nada aí é assinado ou secreto. É o mesmo padrão do QR de maquininha lá fora.
Duas documentações do Banco Central cobrem isso, e vale ter as duas abertas na hora de implementar:
- Manual do BR Code: a adaptação brasileira do padrão EMVCo. Define a estrutura TLV, a lista de campos e o cálculo do CRC16.
-
Manual de Padrões para Iniciação do Pix (testei contra a versão 2.9.0): o perfil específico do Pix em cima do BR Code. É aqui que estão a identificação
br.gov.bcb.pix, os tipos de QR (estático, dinâmico, composto), o formato das chaves e a convenção dotxid.
A estrutura é um TLV (Type-Length-Value). Cada campo é:
ID (2 dígitos) + TAMANHO (2 dígitos) + VALOR
Concatenados, sem separador. Então 5802BR quer dizer: campo 58 (país), tamanho 02, valor BR. Simples assim, e recursivo: alguns campos têm subcampos no mesmo formato dentro do valor.
Um Pix copia e cola estático decodificado fica assim:
Em texto puro, sem os rótulos, é isso aqui concatenado:
00 02 01 Payload Format Indicator (sempre "01")
26 58 Merchant Account Info (Pix)
00 14 BR.GOV.BCB.PIX identificador do arranjo Pix
01 36 1ade9854-82d1-04cd... a chave Pix (aqui uma aleatória / EVP)
52 04 0000 Merchant Category Code (0000 = não informado)
53 03 986 moeda (986 = BRL, ISO 4217)
54 05 45.00 valor da transação
58 02 BR país
59 25 Maria Ferreira Silva Dias nome de quem recebe
60 09 SAO PAULO cidade de quem recebe
62 14 Additional Data
05 10 9kQSS8TQqj txid (identificador da transação)
63 04 198F CRC16 do payload inteiro
É isso. Nenhum campo aí é secreto ou precisa ser assinado por ninguém. Vale fixar dois papéis que voltam o tempo todo daqui pra frente: o PSP recebedor é a instituição de quem gera o QR (a que emite a chave); o PSP pagador é o app de quem vai pagar. A "autenticação" do Pix acontece no PSP pagador: é ele que lê a string, resolve a chave no DICT e mostra o nome do recebedor pra confirmação. O gerador só monta texto; quem aceita ou recusa é o PSP pagador.
O essencial: o payload em ~60 segundos
Esta é a mecânica de montagem, enxuta. Se você já tem um payload e só quer saber por que ele é recusado, pule pra "Válido" não é "aceito". Se quer o passeio campo a campo, o
PixBrCodeGenerator.cse o Manual do BR Code cobrem cada byte.
O coração do gerador são duas funções: uma Tlv, que formata um campo, e uma GerarPayload, que concatena os campos na ordem e fecha com o CRC.
private static string Tlv(string id, string valor)
{
if (valor.Length > 99)
throw new ArgumentException($"Campo {id}: valor com {valor.Length} chars estoura o tamanho de 2 dígitos do EMV.");
return $"{id}{valor.Length:D2}{valor}";
}
public static string GerarPayload(string chavePix, string nomeRecebedor, decimal valor, string? txId, string cidade = "SAO PAULO")
{
var nome = Sanitizar(nomeRecebedor, 25);
var cidadeSanitizada = Sanitizar(cidade, 15);
var referencia = SanitizarTxId(txId);
// Campo 26 é aninhado: identificador do arranjo + a chave Pix
var merchantAccountInfo = Tlv("00", "BR.GOV.BCB.PIX") + Tlv("01", chavePix);
var additionalData = Tlv("05", referencia);
var semCrc =
Tlv("00", "01") + // Payload Format Indicator
Tlv("26", merchantAccountInfo) + // Merchant Account Info (Pix)
Tlv("52", "0000") + // Merchant Category Code
Tlv("53", "986") + // moeda (BRL)
Tlv("54", valor.ToString("F2", CultureInfo.InvariantCulture)) +// valor
Tlv("58", "BR") + // país
Tlv("59", nome) + // nome do recebedor
Tlv("60", cidadeSanitizada) + // cidade do recebedor
Tlv("62", additionalData) + // Additional Data (txid)
"6304"; // id + tamanho do CRC; o valor vem já já
return semCrc + CalcularCrc16(semCrc);
}
Esse é o maior bloco de código do post; daqui pra frente são funções de 3 a 10 linhas.
Cinco detalhes que quebram o payload sem gerar erro nenhum:
-
54(valor): ponto decimal e sempre 2 casas (45.00, nunca45,00). Daí oCultureInfo.InvariantCulture: numa máquina empt-BR,ToString("F2")sozinho devolve vírgula e o payload quebra. -
53(moeda):986, o código ISO 4217 numérico do real. Não éBRL. -
52(MCC):0000serve quando você não é um estabelecimento categorizado. -
26(o bloco Pix): o guard de 99 chars naTlvé o limite genérico do EMV; o perfil Pix aperta o campo26inteiro pra 77 caracteres. GUI (14) + chave EVP (36) + overhead cabe folgado; uma chave de e-mail comprida pode estourar, então vale um teste pra esse caso. -
"6304"no fim: campo63(CRC) + tamanho04; o valor de 4 hex é calculado a seguir, incluindo esses"6304"no cálculo.
O CRC exigido pelo Manual do BR Code é o CRC-16/CCITT-FALSE: polinômio 0x1021, valor inicial 0xFFFF, sem reflexão de bits, sem XOR final, sobre o payload inteiro. É um loop bit a bit. Se a matemática não te interessa, copie a função, confirme que ela passa no vetor de teste logo abaixo e pule pra "Válido" não é "aceito":
public static string CalcularCrc16(string payload)
{
ushort crc = 0xFFFF;
foreach (var b in Encoding.ASCII.GetBytes(payload))
{
crc ^= (ushort)(b << 8);
for (var i = 0; i < 8; i++)
crc = (crc & 0x8000) != 0
? (ushort)((crc << 1) ^ 0x1021)
: (ushort)(crc << 1);
}
return crc.ToString("X4"); // 4 dígitos hex, MAIÚSCULO
}
Ele tem um vetor de teste canônico: 123456789 tem que dar 0x29B1. Se o seu bate com isso, o algoritmo está certo, seja qual for o resto do payload:
[Fact]
public void Crc16_VetorPadrao()
=> Assert.Equal("29B1", PixBrCodeGenerator.CalcularCrc16("123456789"));
"Válido" não é "aceito": as armadilhas
O resumo das três está lá em cima. Aqui é o porquê de cada uma, com o código e as fontes. É onde eu perdi tempo, e é o motivo real deste post existir.
O que destravou foi parar de validar contra ferramentas genéricas e comparar byte a byte com um copia e cola real gerado pelo meu próprio banco. Três diferenças apareceram.
Armadilha 1: a caixa da GUI do arranjo importa na prática
O campo 26, subcampo 00, é a string que identifica o arranjo de pagamento: br.gov.bcb.pix. O Manual do BR Code e o Manual de Padrões para Iniciação do Pix escrevem essa GUI em minúsculo nos exemplos, e há implementações que validam esperando exatamente minúsculo (veja a issue #189 do bacen/pix-api). O CRC não liga pra caixa: br.gov.bcb.pix e BR.GOV.BCB.PIX têm o mesmo tamanho, então o payload continua "estruturalmente válido" e o checksum fecha nos dois casos.
Só que o copia e cola que o meu banco gerava usava BR.GOV.BCB.PIX em maiúsculo, e o leitor de QR dele parecia comparar essa string de forma sensível a caixa pra reconhecer que aquilo era um Pix. Trocar pra maiúsculo foi o que fez o "QR Code inválido" virar a tela de confirmação de pagamento.
Não tenho como afirmar qual caixa é "a certa" pra todo PSP: a spec exibe minúsculo, o leitor do meu banco exigiu maiúsculo. Depois de trocar, testei o mesmo payload nos apps de Banco do Brasil, Bradesco, Itaú, Nubank e Inter: os cinco leram e pagaram. A regra que funcionou pra mim: gere um copia e cola pelo app do próprio banco do recebedor e espelhe exatamente a caixa que ele usa na GUI.
O PixBrCodeGenerator fixa BR.GOV.BCB.PIX em maiúsculo, porque foi o que funcionou aqui. Mas o teste que vale não é "tem que ser maiúsculo": é "tem que bater com o que o banco do recebedor gera". Então eu extraio a GUI de um copia e cola real do banco alvo e comparo com isso:
[Fact]
public void Gui_BateComOCopiaEColaDeReferencia()
{
// subcampo 00 do campo 26, extraído de um "copia e cola" gerado pelo app do banco alvo
// (id "00" + tamanho "14" + valor):
const string guiReferencia = "0014BR.GOV.BCB.PIX";
var p = PixBrCodeGenerator.GerarPayload("chave@exemplo.com", "Fulano", 10m, "x1");
Assert.Contains(guiReferencia, p);
}
Se o banco do seu recebedor gerar a GUI em minúsculo, troque a guiReferencia e o teste continua correto: ele valida consistência com a fonte real, não uma caixa fixa.
Armadilha 2: a chave precisa estar no formato exato do DICT
O subcampo 01 do campo 26 é a chave Pix. Como o PSP pagador é quem resolve a chave (ver a distinção lá em cima), é ele quem vai pegar essa string e procurar no DICT, o diretório de chaves do Banco Central. Se o formato não for exatamente o que o DICT guarda, ele não acha a chave e recusa, mesmo com o BR Code perfeito no resto.
Os formatos, que estão na seção de chaves do Manual de Padrões para Iniciação do Pix:
| Tipo | Formato exigido | Exemplo |
|---|---|---|
| Telefone |
+55 seguido de DDD e número, sem espaço, parênteses ou traço |
+5531999998888 |
| CPF | só os 11 dígitos, sem pontuação | 12345678901 |
| CNPJ | só os 14 dígitos, sem pontuação | 12345678000199 |
| minúsculo | fulano@exemplo.com |
|
| Aleatória (EVP) | o UUID como veio, minúsculo | 1ade9854-82d1-... |
Por isso o bot tem um ChavePixParser que normaliza o que a pessoa digita antes de salvar:
// "telefone (31) 99999-8888" -> "+5531999998888"
private static string? NormalizarTelefone(string valor)
{
var digitos = new string(valor.Where(char.IsDigit).ToArray());
return digitos.Length switch
{
12 or 13 when digitos.StartsWith("55") => "+" + digitos,
10 or 11 => "+55" + digitos,
_ => null,
};
}
Armadilha 3: campo 01 (Point of Initiation Method), omita
O EMV tem um campo 01 no nível raiz (não confundir com o 01 de dentro do 26) chamado Point of Initiation Method: 11 = QR estático (reutilizável), 12 = dinâmico (uso único).
O Manual do BR Code trata esse campo como condicional, e pro QR estático vários geradores de referência simplesmente não colocam. Declarar 12 num QR que é estático é pedir problema. Declarar 11 explícito também é aceito: é o valor certo pra estático, só redundante. Eu omito e nunca deu ruim; se preferir ser explícito, use 11:
// Point of Initiation Method (01) fica de fora de propósito: condicional pra
// estático (11 explícito também vale), e os geradores de referência que eu
// comparei também omitem.
Bônus: nome, cidade e txid não são texto livre
-
Nome (
59) e cidade (60): ASCII imprimível, sem acento, com limite de tamanho (25 e 15)."João Cão"vira"JOAO CAO". Passar um acento aqui é outro jeito de gerar um payload que "quase" funciona. -
txid (subcampo
05do campo62): só alfanumérico, até 25 chars. Quando você não tem um identificador específico pra transação, a convenção do Manual de Padrões é mandar***.
private static string Sanitizar(string texto, int max)
{
var d = texto.Normalize(NormalizationForm.FormD);
var sb = new StringBuilder();
foreach (var c in d)
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark && c < 128)
sb.Append(c);
var r = sb.ToString().ToUpperInvariant().Trim();
return r.Length > max ? r[..max] : r;
}
Virando imagem
O payload é o "copia e cola". Pra virar QR Code é uma lib de imagem. Aqui, QRCoder, 100% local:
public static byte[] GerarPng(string payload)
{
using var gen = new QRCodeGenerator();
using var data = gen.CreateQrCode(payload, QRCodeGenerator.ECCLevel.M);
return new PngByteQRCode(data).GetGraphic(20); // 20 px por módulo
}
Nível de correção de erro M (~15%) já é suficiente pra Pix e é o que costuma ser recomendado. Não precisa de Q ou H, que só incham o QR.
No contexto do bot, essas duas peças (PixBrCodeGenerator e PixQrCodeImageService) são as duas últimas etapas de um fluxo que roda inteiro na sua máquina:
Como testar sem ficar abrindo o app do banco
A estratégia que funcionou pra mim tem três camadas:
-
Vetor canônico do CRC:
123456789vira29B1. Valida o algoritmo isolado. - BR Codes reais conhecidamente válidos: existem listas públicas de payloads Pix usadas justamente pra testar validadores. Recalcular o CRC de um deles e conferir com o valor publicado valida a sua implementação contra um caso real.
- Um copia e cola gerado pelo seu banco: cola num teste, recalcula o CRC, confere. Esse é o que pega as armadilhas de caixa e formato, porque é a "verdade" que o leitor real aceita.
[Fact]
public void Crc_DeUmCopiaECola_Real()
{
// 1. gere um "copia e cola" no app do seu banco
// 2. cole aqui SEM os 4 últimos caracteres (que são o CRC)
// 3. ponha esses 4 caracteres em crcDoBanco
const string semCrc = "<< seu copia e cola, menos os 4 últimos chars >>";
const string crcDoBanco = "XXXX";
Assert.Equal(crcDoBanco, PixBrCodeGenerator.CalcularCrc16(semCrc));
}
E, no fim, o teste que não dá pra automatizar: gerar um payload de R$ 0,01 pra você mesmo e pagar. Uma vez. Vale o centavo.
Quando isso não é suficiente
Antes: as duas formas de QR Pix, lado a lado.
| QR estático | QR dinâmico | |
|---|---|---|
Campo 01 (Point of Initiation Method) |
11 ou omitido |
12 |
| Onde o valor mora | no próprio payload (campo 54) |
numa cobrança criada via API do PSP |
O que o campo 26 carrega |
a chave Pix | uma URL (location) que aponta pra cobrança |
| Precisa de PSP / credencial | não | sim |
| Confirmação de pagamento | nenhuma (você não sabe se foi pago) | notificação/webhook quando a cobrança liquida |
txid |
opcional (convenção: ***) |
obrigatório e único por cobrança |
| Serve pra | "me paga R$ X" fixo, P2P, portfólio | e-commerce, SaaS, cobrança recorrente |
Este post é só sobre o estático, o que dá pra montar sozinho.
Pix estático resolve "gerar uma cobrança". Ele não resolve "saber que foi paga". Não tem webhook, não tem conciliação: o dinheiro cai na conta e ninguém te avisa programaticamente.
No bot eu contornei com um botão "Já paguei" que marca a cobrança manualmente. Pra um grupo de amigos, tudo bem: é confiança mútua e o valor é baixo.
Se você precisa de baixa automática (um SaaS cobrando mensalidade, um e-commerce), aí a história muda:
-
Pix dinâmico via API do seu PSP: em vez da chave, o campo
26carrega uma URL (location) que aponta pra uma cobrança que você criou na API do PSP. Como essa cobrança é um recurso comtxidpróprio e status no PSP, ele consegue te mandar um webhook no instante em que ela é liquidada. É isso que fecha a conciliação, e é justamente o que o estático não tem. - Ou um gateway que abstrai isso.
Mas pra gerar um QR estático de "me paga X", o formato é público, cabe num arquivo, e você não precisa de mais nada além da chave de quem recebe.
Perguntas frequentes
Preciso de um gateway pra gerar um Pix?
Não, pra um QR estático (valor fixo, "me paga X"). O formato é público e você monta o payload com a chave de quem recebe. Gateway ou Pix dinâmico via PSP só é necessário quando você precisa saber, programaticamente, que a cobrança foi paga.
A GUI br.gov.bcb.pix é maiúscula ou minúscula?
Os manuais do Banco Central exibem em minúsculo. Alguns leitores de QR reais comparam de forma sensível a caixa; o meu exigiu maiúsculo. A aposta segura é espelhar a caixa que o copia e cola do próprio banco do recebedor usa.
Como calcular o CRC16 do Pix?
CRC-16/CCITT-FALSE: polinômio 0x1021, valor inicial 0xFFFF, sem reflexão de bits, sem XOR final, sobre o payload inteiro incluindo os 6304 finais. Vetor de teste: 123456789 vira 29B1.
Por que meu Pix copia e cola é "válido" mas o banco recusa?
As três causas mais comuns: a caixa da GUI br.gov.bcb.pix, a chave fora do formato exato do DICT (o banco de quem paga não a encontra) e o campo 01 (Point of Initiation Method) declarado errado num QR estático.
Qual a diferença entre Pix copia e cola estático e dinâmico?
O estático carrega a chave e (opcionalmente) o valor no próprio texto, não precisa de PSP e não avisa quando é pago. O dinâmico carrega uma URL (location) pra uma cobrança criada via API do PSP, exige credencial e devolve notificação na liquidação. Comparação campo a campo na seção "Quando isso não é suficiente".
Qual o formato da chave de telefone no payload?
+55 seguido de DDD e número, sem espaço, parênteses ou traço: +5531999998888.
Qual valor usar no txid quando não tenho um identificador?
***, pela convenção do Manual de Padrões para Iniciação do Pix.
Referências
- Manual do BR Code (Banco Central): estrutura TLV, lista de campos, CRC16.
-
Manual de Padrões para Iniciação do Pix (Banco Central): perfil Pix, GUI
br.gov.bcb.pix, tipos de QR, formato de chave,txid. -
Issue #189 do
bacen/pix-api: discussão sobre validação de CRC16 e EMV entre PSPs. - Especificação EMVCo MPM: EMV QR Code Specification for Payment Systems, Merchant-Presented Mode.
A lição que fica: "válido" e "aceito" são coisas diferentes quando tem um banco de verdade lendo. O CRC e a estrutura TLV você fecha com o manual. A caixa da GUI, o formato da chave no DICT e o campo 01 só aparecem quando você compara byte a byte com um copia e cola gerado pelo app do banco do recebedor. Esse é o teste que importa, e é grátis.
Você já viu um Pix "válido" ser recusado? Qual banco? Comenta aí; quero mapear se a caixa da GUI foi o meu banco sendo específico ou um padrão.
Código completo, com os testes: github.com/victoraugustovalle/RachaContaBot, arquivo RachaContas.Bot/Services/PixBrCodeGenerator.cs.
Esse projeto faz parte do meu portfólio. Se quiser ver os outros (autenticação feita a sério, tempo real com SignalR, integrações de pagamento), está tudo em victoraugusto.dev. Comentário e crítica são bem-vindos.
Testado contra Banco do Brasil, Bradesco, Itaú, Nubank e Inter em 19 de junho de 2026. Regras de PSP mudam sem aviso. Se você está lendo isto muito depois, revalide com o método da seção "Como testar".



Top comments (0)