Pular para o conteúdo
xFlow Payments

Blog

API Pix no Django

Para integrar a API Pix no Django, crie a cobrança numa view com POST /api/v1/charges e Basic auth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste no template e receba o webhook transaction.paid numa view com @csrf_exempt, validando o HMAC-SHA256 de request.body com hmac.compare_digest. Antes da produção, teste com chaves pk_test_.

Atualizado em · Equipe xFlow Payments · 7 min de leitura

No Django, a API Pix se integra com a biblioteca requests e três peças: as credenciais no settings.py, lidas de variáveis de ambiente, uma view que cria a cobrança e entrega o QR Code ao template e uma view que recebe o webhook de pagamento. O código segue a documentação da API, usa Django 4.2 ou superior e a base https://app.xflowpayments.com/api/v1. A versão em Python sem framework, com urllib e conciliação, está em API Pix em Python. A visão geral fica no guia da API Pix.

O que você precisa

  • Uma chave gerada no painel, em Desenvolvedores > Chaves de API. A chave pública (pk_live_...) fica sempre visível. O segredo (sk_live_...) aparece uma única vez.
  • Para testar, uma chave do ambiente Teste (pk_test_... e sk_test_...), que pode ser criada antes da aprovação do cadastro.
  • pip install requests.
  • As três variáveis expostas no settings.py:
python
# settings.py
import os

XFLOW_PUBLIC_KEY = os.environ["XFLOW_PUBLIC_KEY"]
XFLOW_SECRET_KEY = os.environ["XFLOW_SECRET_KEY"]
XFLOW_WEBHOOK_SECRET = os.environ["XFLOW_WEBHOOK_SECRET"]

Com os.environ[...], o servidor nem sobe se faltar uma variável. É melhor descobrir isso no deploy do que no primeiro Pix.

Um cliente para a API

A autenticação é HTTP Basic: usuário é a chave pública, senha é o segredo. Valores vão em centavos, como inteiros. As chaves são lidas do settings dentro da função, e não na importação, para que o override_settings funcione nos testes:

python
# pagamentos/xflow.py
import requests
from django.conf import settings

BASE = "https://app.xflowpayments.com/api/v1"


class XflowErro(Exception):
    def __init__(self, status, corpo, retry_after=None):
        self.status = status
        self.code = corpo.get("error")
        self.request_id = corpo.get("request_id")
        self.retry_after = retry_after
        super().__init__(f"{status} {self.code} ({self.request_id})")


def xflow(method, path, json=None, idempotency_key=None):
    resp = requests.request(
        method,
        BASE + path,
        auth=(settings.XFLOW_PUBLIC_KEY, settings.XFLOW_SECRET_KEY),
        json=json,
        headers={"Idempotency-Key": idempotency_key} if idempotency_key else {},
        timeout=30,
    )
    corpo = resp.json() if resp.content else {}
    if resp.status_code >= 400:
        raise XflowErro(resp.status_code, corpo, resp.headers.get("retry-after"))
    return corpo

Criar a cobrança numa view

python
# pagamentos/views.py
import logging

from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_POST

from .models import Pedido
from .xflow import XflowErro, xflow

log = logging.getLogger(__name__)


@login_required
@require_POST
def gerar_pix(request, pedido_id):
    pedido = get_object_or_404(Pedido, pk=pedido_id, cliente=request.user)
    try:
        cobranca = xflow(
            "POST",
            "/charges",
            json={
                "amountCents": pedido.total_centavos,  # inteiro: R$ 19,90 = 1990
                "customer": {
                    "name": pedido.cliente_nome,
                    "email": pedido.cliente_email,
                    "document": pedido.cliente_documento,  # CPF ou CNPJ, só dígitos
                },
                "description": f"Pedido #{pedido.pk}",
                "external_reference": pedido.referencia,  # ex.: "pedido-1234"
            },
            idempotency_key=pedido.referencia,
        )
    except XflowErro as erro:
        log.warning("xFlow recusou a cobrança: %s", erro)  # traz o request_id
        return render(request, "pagamentos/erro.html", status=502)

    pedido.xflow_charge_id = cobranca["id"]
    pedido.save(update_fields=["xflow_charge_id"])
    return render(request, "pagamentos/pix.html", {"pix": cobranca["pix"]})

Ajuste os nomes dos campos ao seu model. O que importa:

  1. O valor sai do banco. A view recebe só o id do pedido e confere se ele é do usuário logado.
  2. Idempotency-Key por pedido. Um clique duplo devolve a mesma cobrança em vez de gerar outro Pix. A mesma chave com outro corpo dá 409 idempotency_conflict. Detalhes em idempotência em API de pagamento.
  3. external_reference (até 120 caracteres) liga a cobrança ao pedido e volta em toda resposta e webhook. Valor fora do mínimo ou do máximo da conta retorna 400 validation.

Mostrar o QR Code no template

pix.qrCodeBase64 é um PNG em data-URL, pronto para o src. O autoescape do Django não altera esse texto:

html
<!-- pagamentos/templates/pagamentos/pix.html -->
<img src="{{ pix.qrCodeBase64 }}" alt="QR Code Pix" width="240" height="240">
<textarea id="pix" readonly>{{ pix.copyPaste }}</textarea>
<button type="button" onclick="navigator.clipboard.writeText(document.getElementById('pix').value)">
  Copiar código Pix
</button>

pix.copyPaste é o código BR Code que o pagador cola no app do banco: repasse sem alterar. Os dois campos vêm na criação e em GET /api/v1/charges/{id}, mas não na listagem.

Receber o webhook

Cadastre a URL em Integrações > Webhooks. Ela precisa ser https:// e pública. Cada entrega é um POST JSON com os headers x-webhook-event e x-webhook-signature: sha256=..., o HMAC-SHA256 do corpo cru com o segredo do endpoint.

python
# pagamentos/webhooks.py
import hashlib
import hmac
import json

from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from .models import Pedido


def assinatura_valida(corpo: bytes, recebida: str) -> bool:
    segredo = settings.XFLOW_WEBHOOK_SECRET.encode()
    esperada = "sha256=" + hmac.new(segredo, corpo, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperada.encode(), recebida.encode())


@csrf_exempt
@require_POST
def webhook_xflow(request):
    corpo = request.body  # bytes crus, antes de qualquer parse
    if not assinatura_valida(corpo, request.headers.get("x-webhook-signature", "")):
        return HttpResponse(status=401)

    evento = json.loads(corpo)
    data = evento["data"]
    if evento["event"] == "transaction.paid" and data["livemode"]:
        # update condicional: o mesmo evento pode chegar duas vezes
        Pedido.objects.filter(referencia=data["external_reference"], status="aguardando").update(
            status="pago", end_to_end=data["pix"]["endToEnd"]
        )
    return HttpResponse(status=200)
python
# urls.py
from django.urls import path

from pagamentos import views, webhooks

urlpatterns = [
    path("pedidos/<int:pedido_id>/pix/", views.gerar_pix),
    path("webhooks/xflow/", webhooks.webhook_xflow),
]

Os cuidados que são do Django:

  • @csrf_exempt só nesta view. Sem ele, o CsrfViewMiddleware responde 403. A proteção da rota passa a ser a assinatura.
  • request.body, e não request.POST, que fica vazio com JSON. No Django REST Framework, leia request.body antes de tocar em request.data: depois que o DRF consome o corpo, o acesso a request.body gera RawPostDataException.
  • Cadastre a URL com a barra final, igual ao urls.py. Sem ela, o APPEND_SLASH responde com redirecionamento (ou erro, em DEBUG) e o evento não chega à view.
  • hmac.compare_digest compara em tempo constante. Não use == para assinatura.
  • Responda 2xx. Uma exceção vira 500, e a xFlow reenvia até 3 vezes. Por isso o update só muda pedido que ainda está aguardando. livemode: false marca evento de teste.

Se a assinatura não bater, compare os valores no validador de assinatura de webhook. Os outros eventos estão em webhook Pix.

Testar no sandbox

Ponha as chaves de teste nas variáveis e rode no python manage.py shell:

python
from pagamentos.xflow import xflow

teste = xflow("POST", "/charges", json={
    "amountCents": 1990,
    "customer": {"name": "Cliente", "email": "c@exemplo.com", "document": "00000000000"},
})
xflow("POST", f"/test/charges/{teste['id']}/pay")
paga = xflow("GET", f"/charges/{teste['id']}")
print(paga["status"], paga["livemode"])  # paid False

A cobrança vira paid, a taxa sai das taxas reais da sua conta e o webhook chega com livemode: false. O pix.copyPaste de teste não pode ser pago, e a simulação com chave de produção responde 403 test_mode_only. Roteiro completo em sandbox da API Pix.

Para a view do webhook, um teste com o client que exige CSRF prova que o @csrf_exempt está no lugar:

python
# pagamentos/tests.py
import hashlib
import hmac
import json

from django.test import Client, TestCase, override_settings


@override_settings(XFLOW_WEBHOOK_SECRET="segredo-de-teste")
class WebhookXflowTest(TestCase):
    corpo = json.dumps({"event": "transaction.pending", "data": {"livemode": False}})

    def enviar(self, assinatura):
        return Client(enforce_csrf_checks=True).post(
            "/webhooks/xflow/", self.corpo, content_type="application/json",
            HTTP_X_WEBHOOK_SIGNATURE=assinatura,
        )

    def test_assinatura_certa(self):
        sig = hmac.new(b"segredo-de-teste", self.corpo.encode(), hashlib.sha256).hexdigest()
        self.assertEqual(self.enviar("sha256=" + sig).status_code, 200)

    def test_assinatura_errada(self):
        self.assertEqual(self.enviar("sha256=errada").status_code, 401)

Erros e limite de requisições

A view acima já captura o XflowErro e registra o request_id, que localiza a chamada em Integrações > Logs da API por 30 dias. Os códigos mais comuns:

  • 401 unauthorized: chave inválida ou revogada, ou IP fora da allowlist.
  • 403 account_inactive: conta em análise ou bloqueada. Siga com a chave de teste.
  • 429 rate_limited: o limite é por conta. Numa view, não segure o worker com time.sleep: mostre uma mensagem e deixe o cliente tentar de novo. A mesma Idempotency-Key garante que a nova tentativa não gera outro Pix. Em tarefas de fundo, espere os segundos de erro.retry_after.
  • 502 charge_failed: falha ao gerar a cobrança no provedor. Repita com backoff.

Toda resposta autenticada traz x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset. A tabela completa está em erros da API Pix.

Antes de ir para produção

  1. Troque as chaves de teste por pk_live_/sk_live_ só nas variáveis do servidor de produção.
  2. Cadastre a URL de produção do webhook, com a barra final, e guarde o segredo do endpoint.
  3. Confirme que só a view do webhook tem @csrf_exempt.
  4. Mantenha o filtro de livemode e o update condicional.
  5. Registre o request_id de toda resposta com erro e, se quiser, ative a allowlist de IP em Chaves de API.

Prefira o webhook ao polling de GET /api/v1/charges/{id}. A versão assíncrona deste fluxo está em API Pix no FastAPI, e a página Desenvolvedores reúne chaves, logs e guias.

Perguntas frequentes

Por que o webhook Pix responde 403 no Django?

Porque a view passa pelo CsrfViewMiddleware, que recusa POST sem token de CSRF, e a xFlow não envia esse token. Marque só a view do webhook com @csrf_exempt. Quem protege a rota é a assinatura HMAC do header x-webhook-signature.

Posso usar request.POST ou request.data para validar o webhook?

Não. Com corpo JSON, request.POST fica vazio, e o request.data do Django REST Framework já vem convertido. O HMAC é calculado sobre os bytes exatos recebidos: valide com request.body e só depois faça json.loads.

Onde guardar as chaves da API Pix no Django?

Em variáveis de ambiente, lidas no settings.py com os.environ. O segredo sk_live_ e o segredo do webhook não vão para o repositório, para templates nem para o JavaScript enviado ao navegador.

Como testar a integração Pix no Django sem pagar?

Com chaves de teste (pk_test_/sk_test_), crie a cobrança no python manage.py shell e chame POST /api/v1/test/charges/{id}/pay. A cobrança vira paid e o webhook transaction.paid chega com livemode: false.

Continue lendo

SoluçãoAPI PixA API Pix da xFlow Payments é uma API REST para criar cobranças Pix com QR Code e copia e cola, estornar, sacar e receber webhooks assinados. Uma única integração conecta a sua operação a 11 adquirentes, com roteamento por regras e retentativa automática.BlogAPI Pix em PythonPara integrar a API Pix em Python, envie POST /api/v1/charges com requests e auth=(pk_live_, sk_live_), mostre o pix.qrCodeBase64 e o pix.copyPaste da resposta e confirme o pagamento pelo webhook transaction.paid, validando o HMAC-SHA256 do corpo cru com hmac.compare_digest. No sandbox, use chaves pk_test_ e simule o pagamento.BlogWebhook Pix: eventos, assinatura HMAC e boas práticasWebhook Pix é o aviso que a API envia para a sua URL quando algo acontece com uma cobrança ou um saque, como o pagamento confirmado. Na xFlow, cada aviso é um POST JSON assinado com HMAC-SHA256, reenviado até 3 vezes até você responder 2xx.BlogAPI Pix sandbox: como testar sem mover dinheiroO sandbox da API Pix da xFlow funciona com chaves pk_test_ e sk_test_: mesmos endpoints, mesmas respostas com livemode: false, nenhum dinheiro real e um endpoint para simular o pagamento que dispara o webhook transaction.paid.BlogAPI Pix no FastAPIPara integrar a API Pix no FastAPI, use um httpx.AsyncClient com auth=(pk_live_, sk_live_) para enviar POST /api/v1/charges, devolva o pix.qrCodeBase64 e o pix.copyPaste ao front-end e receba o webhook transaction.paid lendo await request.body(), validando o HMAC-SHA256 com hmac.compare_digest e deixando o processamento para BackgroundTasks. Teste antes com chaves pk_test_.