Pagamentos

PIX Cobrança em SaaS: Reconciliação Automática e Tratamento de Status

Automatize a entrada de caixa do seu SaaS sem planilhas, implementando lógica de reconciliação atômica que lida corretamente com pagamentos concorrentes.

Eduardo Soliveira
Eduardo SoliveiraEditor-Chefe de RegTech e Segurança
Imagem editorial ilustrando PIX Cobrança em SaaS: Reconciliação Automática e Tratamento de Status

A conciliação manual de pagamentos matou a escalabilidade de mais startups brasileiras do que a falta de capital. No modelo de assinatura B2B, onde faturas de R$ 12.000,00 precisam ser reconhecidas em segundos para liberar acesso à plataforma, contar com um analista exportando CSVs do banco não é opção. O PIX Cobrança resolve a liquidação financeira (T+0), mas a dor de cabeça real move-se para o backend: garantir que o estado do seu sistema reflita a realidade do banco central sem janelas de inconsistência.

O erro clássico é confiar que o usuário clicou no botão "Pagar" no app do banco. Ele pode ter gerado a chave pix, clicado fora da tela e deixado a transação pendente por horas. Seu sistema só pode liberar o serviço quando o webhook chegar, e ele precisa ser processado com uma lógica de estado à prova de falhas. A documentação oficial do Banco Central e dos PSPs (Instituições de Pagamento) sobre o fluxo de pending para confirmed é clara, mas a implementação correta exige rigidez no tratamento de concorrência.

Aqui está o processo técnico para blindar sua receita.

A armadilha de confiar apenas na resposta síncrona da criação

Quando seu backend chama o endpoint POST /v2/cob da API do seu PSP, a resposta retorna o loc (local do QR Code) e o status criado ou ativo. Muitos desenvolvedores, por ansiedade, atualizam o status da fatura no banco de dados para "aguardando pagamento" e voltam a tela para o usuário, exibindo o QR Code. O problema surge quando o cliente faz o pagamento 30 segundos depois, mas o webhook só chega 2 minutos depois devido a uma latência na rede do PSP.

Detalhe fotográfico relacionado a PIX Cobrança em SaaS: Reconciliação Automática e Tratamento de Status

Se você consultou o status via API polling (chamadas periódicas GET) para acelerar o processo, criou uma race condition: a requisição de consulta chega 500ms antes do webhook. Ambos veem o pagamento como concluido. Ambos tentam atualizar o saldo do cliente e enviar o e-mail de boas-vindas. O resultado? E-mail duplicado e, se não houver trava no banco de dados, créditos em dobro no sistema. Em 2026, com APIs de Banking-as-a-Service (BaaS) integradas diretamente ao SaaS, esse tipo de erro é inaceitável, pois mexe com tesouraria real.

Portanto, o passo zero é aceitar que a API de criação é síncrona, mas a confirmação é um evento assíncrono. Seu banco de dados deve tratar o pagamento como pending_payment imediatamente após a criação da cobrança, e só migrar para paid sob estrita condicionalidade do ID da transação (e2eId).

Validação de segurança: assinatura HMAC é obrigatória

Nunca processe um webhook apenas porque ele chegou na sua rota /api/webhooks/pix. Qualquer pessoa na internet pode disparar um POST para esse endpoint com o payload { "status": "CONCLUIDA" }. Se seu código der crédito nessa condição, você sofrerá um ataque massivo de fraudes que vai drenar seu caixa em minutos.

Todo PSP respeitável envia um cabeçalho HTTP, geralmente chamado x-signature ou digital-signature, contendo um hash HMAC SHA-256 do payload usando uma chave secreta que só você e o PSP conhecem.

A lógica de validação deve ser o middleware (intermediário) da sua rota:

  1. Pegue o "body" bruto da requisição (não o objeto JSON parseado) para garantir que a formatação de espaço ou quebra de linha não altere o hash.
  2. Concatene o timestamp (se enviado) com o payload bruto.
  3. Compute o HMAC SHA-256 usando a WEBHOOK_SECRET_KEY que você configurou no dashboard do PSP.
  4. Compare o hash gerado com o header recebido. Use uma função de comparação constante-time (como crypto.timingSafeEqual em Node.js) para evitar ataques de timing.
  5. Se não bater, retorne HTTP 401 imediatamente. Se bater, processe.

Ignorar esse passo é uma violação grave de segurança regulatória. O PIX caiu no gosto popular justamente pela liquidez, e isso atrai golpistas.

Estrutura do banco de dados para transições de estado atômicas

Para implementar a reconciliação automática, sua tabela de transações (financial_transactions) não pode ter colunas de status soltas. Você precisa de uma coluna status (enum: created, pending, confirmed, failed, canceled) e, crucialmente, uma coluna pix_e2e_id que aceite nulos e tenha um índice único.

O e2eId (End-to-End ID) é o identificador universal da transação PIX no Banco Central. Ele é a única garantia de idempotência. Quando o webhook chega, ele trás esse campo.

Exemplo de fluxo SQL seguro dentro de uma transação:

BEGIN TRANSACTION;

-- 1. Tenta bloquear a linha para evitar leitura suja (SELECT FOR UPDATE)
SELECT * FROM financial_transactions 
WHERE pix_e2e_id = 'E1823612021625214132101103123412' 
FOR UPDATE;

-- 2. Se não existe, insere (caso seu sistema crie a transação na resposta do webhook, o que não recomendo para PIX Cobrança, mas é comum em Key PIX estático)
-- OU, no caso de PIX Cobrança onde a transação já existe com status 'pending':

UPDATE financial_transactions 
SET status = 'confirmed', 
    confirmed_at = NOW(), 
    pix_e2e_id = 'E1823612021625214132101103123412'
WHERE id = 4589 AND status IN ('pending', 'created');

-- 3. Verifica se o update aconteceu. Se o rowCount for 0, significa que essa transação já estava confirmada ou não existe.
COMMIT;

Essa estrutura resolve o problema de webhooks duplicados. Os PSPs enviam o mesmo evento múltiplas vezes se não recebem um HTTP 200 OK. Se você tentar atualizar uma linha que já está confirmed com a lógica acima (ou se o e2eId já estiver preenchido e confirmado em outra linha), o update simplesmente não afeta linhas, e seu código pode ignorar o processamento secundário (envio de email, liberação de crédito), mantendo a integridade dos dados.

Detalhe fotográfico relacionado a PIX Cobrança em SaaS: Reconciliação Automática e Tratamento de Status

A imagem acima ilustra o ambiente real onde isso acontece: monitorando logs para garantir que o e2eId está sendo salvo corretamente. Perder esse ID na criação da cobrança é um erro comum que torna a reconciliação impossível.

Processando o evento 'CONCLUIDA' e o perigo da race condition

Chegamos no núcleo da lógica B2B. Ao receber o payload com status CONCLUIDA, seu backend deve:

  1. Validar o valor recebido no webhook contra o valor da fatura na sua base. Ataques de man-in-the-middle ou erros de integração raramente ocorrem, mas a verificação de valor é barata e salva muito dinheiro. Um webhook dizendo que você recebeu R$ 50,00 para uma fatura de R$ 5.000,00 deve ser descartado/logado como erro.
  2. Iniciar a transação atômica no banco (conforme descrito acima).
  3. Atualizar o status.
  4. Enfileirar eventos de consequência (publicar no RabbitMQ/SQS/SNS). Não envie o e-mail de "Obrigado pelo pagamento" dentro do controller do webhook. Isso atrasa a resposta ao PSP. Se o seu sistema de e-mail demorar 2 segundos, o PSP vai achar que seu endpoint caiu e reenviar o webhook, gerando duplicidade na fila de mensagens. Retorne HTTP 200 o mais rápido possível e delegue o trabalho pesado para workers assíncronos.

O tratamento de status pending geralmente ocorre quando o QR Code foi gerado, mas o pagamento ainda não passou pela centralização do BCB. Algumas APIs enviam ATIVA, outras PENDING. Para sua lógica interna, trate ambos como "Aguardando Liquidação". O PIX é finalidade instantânea, mas existe um janelinha técnica. Não considere o dinheiro como seu até o CONCLUIDA (ou REMOVIDA_PELO_USUARIO_RECEBEDOR em casos de devolução, embora raro em cobrança imediata).

Estratégia de fallback com consulta manual (Polling)

Webhooks podem falhar. Seu servidor cai, seu certificado SSL expira ou o PSP tem problemas de roteamento. Se você deixar apenas o webhook cuidar da conciliação, sua fatura ficará em aberto eternamente até que um cliente reclame.

Você precisa de um cron job ou scheduler que rode a cada 5 ou 10 minutos. A tarefa dele é simples mas vital:

  1. Buscar transações com status pending criadas a mais de 15 minutos.
  2. Para cada uma, fazer uma chamada GET /v2/cob/{txid} na API do PSP.
  3. Se a resposta vier como CONCLUIDA ou PAGO, dispara a mesma lógica de atualização do banco que o webhook usaria.

Isso cria uma rede de segurança. Se o webhook falhar, o polling "corrige" o estado em, no máximo, 15 minutos. Uma ressalva técnica: tome cuidado com rate limiting. Se você tiver 10.000 pagamentos pendentes num dia de Black Friday, seu cron job pode ser bloqueado pela API do PSP. Ajuste a janela de busca para, no máximo, as últimas 2 horas, e filtre apenas os txid mais críticos.

Lidando com estornos e devoluções no fluxo B2B

O PIX permite devolução (estorno) pelo pagador ou recebedor em até 90 dias. No contexto B2B, seu cliente (a empresa pagadora) pode pedir o chargeback se o serviço não foi entregue, ou o banco pode identificar uma fraude.

Os webhooks de devolução chegam com o status DEVOLVIDA. Sua lógica aqui deve ser ainda mais rígida. Ao receber esse evento:

  1. Bloqueie o acesso do cliente imediatamente (se o valor devolvido zerar o saldo).
  2. Registre o motivo da devolução (ID da devolução na API).
  3. Notifique financeiramente seu time ou, se for SaaS automatizado, estorne os créditos da conta SaaS.

A maioria dos desenvolvedores foca apenas no "pagamento aceito" e esquece o "pagamento cancelado". Em 2026, ferramentas de conciliação financeira inteligentes já são padrão em ERPs como Totvs e RM, mas se o seu SaaS não fizer a escrituração reversa automaticamente, seu balanço não fechará.

Construir um sistema de pagamentos robusto no Brasil exige respeitar as idiossincrasias do PIX. Ele é rápido, mas a assincronicidade das notificações é o ponto de estrangulamento. Se seu backend tratar o webhook como uma "dica" de status e não como a "verdade absoluta", você evita o erro clássico de liberar serviço para um cliente que vai ter o pagamento estornado no dia seguinte. Implemente travas de banco, valide a assinatura do payload e mantenha o job de polling ativo. A automação só traz tranquilidade quando o sistema sabe lidar com o caos.

Leia em seguida