Integração de Pagamentos com PIX em Python: Guia Prático

Aprenda a integrar pagamentos por PIX em Python: criar cobrança com QR Code e Copia e Cola, receber webhooks com FastAPI, validar assinatura, garantir idempotência e segurança.

9 min de leitura Equipe Python Brasil

Integrar pagamentos por PIX em Python é hoje uma das habilidades mais pedidas em vagas de backend e fintech no Brasil. Desde 2020, o PIX virou o meio de pagamento padrão para transferências imediatas, e quase todo sistema que vende algo — e-commerce, SaaS, assinatura, marketplace, doação — precisa gerar uma cobrança, mostrar um QR Code e confirmar o pagamento de forma automática.

A boa notícia é que a integração não é difícil do ponto de vista de código: o fluxo gira em torno de chamadas HTTP, webhooks e uma chave de idempotência. O desafio real está nos detalhes que importam em produção — validar a assinatura do webhook, evitar cobrança duplicada, tratar concorrência, guardar segredos de forma segura e conciliar o que foi pago com o que foi prometido. Neste guia vamos montar esse fluxo inteiro em Python, com FastAPI para o webhook e boas práticas que valem para qualquer PSP (provedor de serviço de pagamento).

Como o PIX funciona para quem integra

Do ponto de vista técnico, o fluxo de uma cobrança PIX tem quatro peças:

  1. Cobrança: você cria uma cobrança no PSP e recebe um txid (identificador único da transação).
  2. BR Code / Copia e Cola: o PSP devolve uma string no padrão EMV — o famoso “PIX Copia e Cola” — que o cliente cola no app do banco.
  3. QR Code: a mesma string pode virar uma imagem de QR Code, lida pela câmera do celular.
  4. Webhook de confirmação: quando o cliente paga, o PSP chama o seu endpoint avisando que o txid foi liquidado.

O detalhe que confunde muita gente: a confirmação nunca vem como resposta direta da criação da cobrança. A criação só reserva a cobrança; o dinheiro chega depois, de forma assíncrona, via webhook. Por isso, tratar webhook com idempotência é a parte mais importante de toda a integração.

Banco Central direto ou PSP?

Você pode integrar o PIX de duas formas. A escolha muda bastante o nível de esforço:

CaminhoQuando usarExige
Banco Central direto (API Pix)Instituições financeiras, fintechs com conta institucionalCertificado ICP-Brasil, mTLS, conta regulada pelo BCB
PSP (Mercado Pago, Asaas, Gerencianet, Pagar.me)E-commerce, SaaS, startups, qualquer empresaConta no PSP, access_token, HTTPS no webhook

Para a esmagadora maioria dos projetos em Python, o caminho é o PSP. Ele abstrai o certificado, o mTLS e a comunicação direta com o Bacen, e expõe uma API REST simples com HTTPS. Vamos seguir por aqui.

Preparando o ambiente

Comece com um ambiente isolado, como já mostramos no guia de ambientes virtuais em Python. Com uv, o setup é rápido:

uv init pix-python
cd pix-python
uv add fastapi "uvicorn[standard]" httpx pydantic qrcode python-dotenv

Use HTTPX como cliente HTTP: ele suporta async, timeouts configuráveis e retries, fundamentais para não travar quando o PSP demora a responder. Para guardar chaves de API com segurança, leia o guia de variáveis de ambiente com python-dotenv — nunca coloque access_token no código.

Criando uma cobrança com QR Code e Copia e Cola

A estrutura de uma cobrança é parecida entre os PSPs. O exemplo abaixo usa um cliente HTTPX genérico que funciona com a maioria das APIs. Ajuste o base_url e o corpo para o PSP que você escolher (Mercado Pago, Asaas, Gerencianet e Pagar.me seguem o mesmo padrão geral):

import os
import httpx
from dataclasses import dataclass

PSP_BASE_URL = os.environ["PSP_BASE_URL"]      # ex.: https://api.asaas.com ou https://api.mercadopago.com
PSP_TOKEN = os.environ["PSP_TOKEN"]            # access_token do PSP

@dataclass
class Cobranca:
    txid: str
    copia_e_cola: str           # string "PIX Copia e Cola" (BR Code)
    qr_code_base64: str         # imagem do QR Code em base64

async def criar_cobranca(cliente_id: str, valor: float, descricao: str) -> Cobranca:
    headers = {"Authorization": f"Bearer {PSP_TOKEN}", "Content-Type": "application/json"}
    payload = {
        "customer": cliente_id,
        "billingType": "PIX",
        "value": valor,
        "description": descricao,
    }
    async with httpx.AsyncClient(timeout=10.0) as client:
        resp = await client.post(f"{PSP_BASE_URL}/payments", json=payload, headers=headers)
        resp.raise_for_status()
        data = resp.json()

    # Cada PSP nomeia os campos de forma ligeiramente diferente:
    #   Asaas:       payload (Copia e Cola), encodedImage (QR Code base64)
    #   Mercado Pago:  qr_code / ticket_url + qr_code_base64
    return Cobranca(
        txid=data["id"],
        copia_e_cola=data.get("payload") or data.get("qr_code"),
        qr_code_base64=data.get("encodedImage") or data.get("qr_code_base64", ""),
    )

Salve o txid no banco associado ao pedido. Ele é a sua chave de idempotência: qualquer evento futuro (webhook, consulta de status, reembolso) referencia esse mesmo identificador.

Para gerar o QR Code localmente a partir do Copia e Cola (útil quando o PSP só devolve a string), use a biblioteca qrcode:

import qrcode

def salvar_qrcode(copia_e_cola: str, caminho: str) -> None:
    img = qrcode.make(copia_e_cola)
    img.save(caminho)

Recebendo o webhook de confirmação com FastAPI

A peça central da integração é o endpoint que recebe a notificação de pagamento. O PSP chama esse URL sempre que uma cobrança é liquidada — e pode chamar mais de uma vez para a mesma transação (retry em falha de rede). Por isso, idempotência é obrigatória.

O exemplo abaixo usa FastAPI e segue o mesmo padrão de robustez que mostramos no guia de webhooks com FastAPI:

from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from pydantic import BaseModel

app = FastAPI()

# status por txid — na prática, um banco de dados
status_pedidos: dict[str, str] = {}

class NotificacaoPix(BaseModel):
    event: str
    payment: dict | None = None

@app.post("/webhook/pix")
async def webhook_pix(req: Request, background: BackgroundTasks):
    # 1. Validar assinatura antes de confiar no conteúdo
    if not assinatura_valida(req):
        raise HTTPException(status_code=401, detail="Assinatura inválida")

    body = await req.json()
    txid = body.get("payment", {}).get("id")
    if not txid:
        raise HTTPException(status_code=400, detail="txid ausente")

    # 2. Idempotência: ignorar se já processado
    if status_pedidos.get(txid) == "paid":
        return {"status": "ok", "duplicado": True}

    # 3. Confirmar de forma resiliente, fora do ciclo do webhook
    background.add_task(confirmar_pagamento, txid)
    return {"status": "ok"}

async def confirmar_pagamento(txid: str) -> None:
    # Consulte o PSP para confirmar o status real (não confie só no webhook)
    status = await consultar_status_no_psp(txid)
    if status == "paid":
        status_pedidos[txid] = "paid"
        # aqui: liberar pedido, enviar e-mail, atualizar estoque...

Do pontos críticos desse código:

  • Sempre valide a assinatura do webhook (header de assinatura/HMAC enviado pelo PSP). Sem isso, qualquer um pode simular um pagamento pago.
  • Sempre confirme consultando o PSP antes de liberar o pedido. O webhook avisa; a consulta confirma. Isso protege contra webhooks forjados e contra falsos positivos.
  • Processe de forma assíncrona (BackgroundTasks ou uma fila). O webhook precisa responder rápido (200 OK), senão o PSP faz retry e você processa o mesmo evento várias vezes.

Segurança que não pode faltar

Integração financeira é segurança aplicada. Some pontos não negociáveis:

  • Chaves em variáveis de ambiente, nunca no código nem no Git. Use python-dotenv local e o cofre de segredos do provedor (AWS Secrets Manager, Cloudflare, etc.) em produção.
  • Valide a assinatura de todo webhook. Cada PSP documenta qual header carrega a assinatura e qual segredo usar no HMAC.
  • Não registre dados sensíveis em log: CPF, e-mail, número de cartão, chave PIX de terceiros. O próprio Bacen recomenda anonimizar. O guia de conciliação financeira com Python mostra como usar dados sintéticos em testes para nunca vazar informação real.
  • Use HTTPS em todo o caminho, do PSP até o seu banco de dados.

Conciliação, testes e erros comuns

Mesmo com webhook perfeito, você ainda precisa conciliar periodicamente: comparar o que o PSP diz que foi pago com o que o seu sistema registrou. PIX pode cair no mesmo dia, mas aparecer com descrição diferente; webhook pode se perder; cliente pode pagar após o vencimento. Esse casamento de dados é exatamente o que cobrimos no guia de conciliação financeira — leia em conjunto.

Para testar, cubra o fluxo inteiro com pytest: criação de cobrança, tratamento de webhook válido, webhook duplicado (idempotência) e assinatura inválida (rejeição). Os PSPs oferecem ambientes de sandbox com chaves de teste — use-os sempre.

Erros comuns que valem o checklist final:

  • confiar só no webhook sem consultar o PSP (falso positivo);
  • não tratar txid duplicado (cobrança processada duas vezes);
  • responder o webhook com lentidão (PSP dispara retries);
  • deixar o access_token no repositório (vazamento de credencial);
  • não validar a assinatura (qualquer pessoa simula pagamento).

Portfólio e carreira

Uma integração de PIX ponta a ponta — cobrança, QR Code, webhook com idempotência, validação de assinatura e conciliação — é um dos projetos mais fortes que você pode colocar em um portfólio para vagas de backend Python. Ela mostra domínio de APIs assíncronas, segurança aplicada, modelagem de dados e atenção a detalhes de produção. Se quiser montar isso como peça de portfólio, o guia de projetos de portfólio em Python traz a estrutura certa, e o roadmap Python 2026 mostra onde pagamentos se encaixam na trilha de carreira.

Vagas de fintech e backend frequentemente listam “integração de meios de pagamento” como requisito — confira as oportunidades na nossa página de vagas de Python em fintech. Dominar esse fluxo coloca você à frente de boa parte dos candidatos.

Conclusão

Integrar PIX em Python é, no fundo, um exercício de disciplina: HTTPX para criar a cobrança, QR Code para o cliente, webhook com FastAPI para confirmar, idempotência por txid para evitar duplicidade e validação de assinatura para segurança. O código é curto; o que sustenta a integração em produção são as decisões de idempotência, conciliação e proteção de dados. Comece com um PSP em sandbox, monte o fluxo completo ponta a ponta e adicione testes e conciliação antes de qualquer coisa ir ao ar.

Perguntas frequentes

Preciso de certificado digital para integrar PIX em Python?

Depende do caminho. A integração direta com o Banco Central (API Pix) exige conta institucional e certificado ICP-Brasil com mTLS, o que não se aplica a pessoas físicas ou empresas comuns. Integrar via PSP (Mercado Pago, Asaas, Gerencianet, Pagar.me) usa apenas HTTPS e access_token, sem certificado.

Como evito processar o mesmo pagamento duas vezes?

Use o txid da cobrança como chave de idempotência e persista o status do pagamento no banco antes de atualizar o pedido. Se o webhook chegar repetido, a segunda ocorrência encontra o status já confirmado e é ignorada.

Qual biblioteca Python uso para gerar o QR Code do PIX?

A maioria dos PSPs já retorna a string Copia e Cola e o QR Code em base64 na resposta da cobrança, então você apenas salva a imagem. Para gerar o BR Code localmente, a biblioteca qrcode do Python monta a imagem a partir do payload.

Posso receber webhook de PIX em produção sem domínio HTTPS?

Não. O PSP exige um endpoint público com HTTPS válido para entregar a notificação. Em desenvolvimento, use um túnel como ngrok ou Cloudflare Tunnel para expor o FastAPI local.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português