Mandar link de pagamento por WhatsApp converte mais que email. Mandar QR Code PIX direto no chat converte ainda mais — porque o cliente não precisa sair do app, basta abrir o WhatsApp Web em outra aba ou usar o "PIX Copia e Cola" diretamente do chat. Em testes A/B com lojas que migraram de "link externo" para "QR PIX no chat", a taxa de conversão subiu de 12% para 34%.
Este artigo mostra a integração completa: gerar QR Code PIX, enviar como imagem pelo WhatsApp, receber webhook de confirmação e atualizar status do pedido. Código pronto para produção.
Arquitetura
Cinco passos:
- Cliente manda mensagem no WhatsApp pedindo cobrança ("quero pagar")
- Bot recebe via webhook da ZAP API
- Bot chama gateway PIX (qualquer gateway brasileiro com API REST) para criar cobrança
- Bot envia QR Code como imagem + PIX Copia e Cola como texto
- Gateway dispara webhook quando pagamento confirma → bot avisa cliente que recebeu
Estrutura do payload de cobrança PIX
Independente do gateway escolhido, o fluxo é parecido. Você cria uma cobrança e recebe de volta:
txid— identificador único da cobrançabrCode— string EMV (PIX Copia e Cola)qrCodeUrlouqrCodeBase64— imagem do QRexpirationSeconds— segundos até expirar (padrão 1800 = 30min)
Código: criar cobrança e enviar pelo WhatsApp
// pix-bot.js
import axios from "axios";
import express from "express";
const app = express();
app.use(express.json());
const ZAP = axios.create({
baseURL: "https://api.zap-api.tech/v1",
headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});
const PIX = axios.create({
baseURL: process.env.PIX_GATEWAY_URL,
headers: { Authorization: `Bearer ${process.env.PIX_TOKEN}` },
});
// Recebe webhook de mensagem do cliente
app.post("/webhook/zap", async (req, res) => {
res.status(200).send("OK");
const evento = req.body;
if (evento.event !== "message.received") return;
const texto = evento.message.text?.toLowerCase() || "";
const remetente = evento.message.from;
if (texto.includes("pagar") || texto.includes("pix")) {
await gerarEEnviarCobranca({ telefone: remetente, valorCentavos: 9900 });
}
});
async function gerarEEnviarCobranca({ telefone, valorCentavos }) {
// 1. Cria cobrança no gateway
const { data: cobranca } = await PIX.post("/cobrancas", {
valor: { original: (valorCentavos / 100).toFixed(2) },
chave: process.env.PIX_CHAVE,
solicitacaoPagador: "Pedido #" + Date.now(),
expiracaoSegundos: 1800,
idempotencyKey: `pix-${telefone}-${Date.now()}`,
});
// 2. Envia QR Code como imagem
await ZAP.post(`/instances/${process.env.ZAP_INSTANCE}/messages`, {
type: "image",
to: telefone,
url: cobranca.qrCodeUrl,
caption: `Pague R$ ${(valorCentavos / 100).toFixed(2).replace(".", ",")} via PIX`,
});
// 3. Envia BR Code para copia e cola
await ZAP.post(`/instances/${process.env.ZAP_INSTANCE}/messages`, {
type: "text",
to: telefone,
text: `PIX Copia e Cola:\n\n${cobranca.brCode}\n\nO código expira em 30 minutos.`,
});
// 4. Persiste relação txid → telefone (para webhook de pagamento)
await db.query(
"INSERT INTO cobrancas_pendentes (txid, telefone, valor_centavos, criado_em) VALUES ($1, $2, $3, NOW())",
[cobranca.txid, telefone, valorCentavos]
);
}
app.listen(3000);
Código: webhook de confirmação de pagamento
app.post("/webhook/pix", async (req, res) => {
res.status(200).send("OK");
const { txid, status } = req.body;
if (status !== "PAGO") return;
// Busca o telefone associado ao txid
const { rows: [pendente] } = await db.query(
"SELECT telefone, valor_centavos FROM cobrancas_pendentes WHERE txid = $1",
[txid]
);
if (!pendente) return;
// Avisa cliente
const valorFormatado = (pendente.valor_centavos / 100)
.toFixed(2)
.replace(".", ",");
await ZAP.post(`/instances/${process.env.ZAP_INSTANCE}/messages`, {
type: "text",
to: pendente.telefone,
text: `✅ Pagamento de R$ ${valorFormatado} confirmado!\n\nObrigado. Seu pedido entrou em processamento.`,
});
await db.query(
"UPDATE cobrancas_pendentes SET status = 'pago', pago_em = NOW() WHERE txid = $1",
[txid]
);
});
Idempotência: evitar cobrar 2x
Cliente clica 2x no botão de pagar, ou a mensagem chega duplicada. Sem idempotência você gera 2 cobranças. Resolução:
// Antes de criar cobrança, checa se já existe ativa para esse telefone+valor
const { rows: existente } = await db.query(`
SELECT txid FROM cobrancas_pendentes
WHERE telefone = $1 AND valor_centavos = $2 AND status = 'pendente'
AND criado_em > NOW() - INTERVAL '30 minutes'
`, [telefone, valorCentavos]);
if (existente.length > 0) {
// Reenvia o QR existente em vez de criar novo
return reenviarCobranca(existente[0].txid);
}
Código: imagem em base64 (alternativa)
Se o gateway não retorna URL de imagem, mas sim base64:
await ZAP.post(`/instances/${process.env.ZAP_INSTANCE}/messages`, {
type: "image",
to: telefone,
base64: cobranca.qrCodeBase64, // sem prefixo data:image/png;base64,
mimetype: "image/png",
caption: `Pague R$ ${valor} via PIX`,
});
Tratamento de expiração
QR PIX expira (padrão 30min). Se cliente abrir mensagem antiga e tentar pagar, vai falhar. Solução: cron que checa pendentes expirando e oferece renovar.
// Roda a cada 5 minutos
async function avisarExpiracao() {
const { rows } = await db.query(`
SELECT txid, telefone FROM cobrancas_pendentes
WHERE status = 'pendente'
AND criado_em > NOW() - INTERVAL '30 minutes'
AND criado_em < NOW() - INTERVAL '25 minutes'
AND aviso_expiracao_em IS NULL
`);
for (const c of rows) {
await ZAP.post(`/instances/${process.env.ZAP_INSTANCE}/messages`, {
type: "text",
to: c.telefone,
text: "Seu PIX expira em 5 minutos. Quer que eu gere um novo? Responda SIM.",
});
await db.query(
"UPDATE cobrancas_pendentes SET aviso_expiracao_em = NOW() WHERE txid = $1",
[c.txid]
);
}
}
Curl de teste
# Simula cliente pedindo cobrança
curl -X POST https://seu-bot.com.br/webhook/zap \
-H "Content-Type: application/json" \
-d '{
"event": "message.received",
"message": {
"from": "5511999998888",
"text": "quero pagar"
}
}'
FAQ
Existe limite de valor para cobrança PIX?
Por padrão, R$ 1,00 a R$ 50.000,00 por transação na maioria dos gateways (configurável pelo banco). Para valores maiores, contrato dedicado com seu banco.
QR expirou. O cliente pode pagar mesmo assim?
Não. PIX com expiração rejeita pagamento após o prazo. Por isso o cron de aviso é importante — você renova antes do cliente tentar pagar e ver erro.
Como faço conciliação financeira?
Cada gateway PIX expõe relatório de pagamentos por dia/período. Cruze com sua tabela cobrancas_pendentes usando txid como chave. Diferenças indicam webhook perdido.
Posso fazer split de pagamento (PIX para mim + PIX para parceiro)?
Depende do gateway. Alguns suportam split automático nativo (você cria cobrança com array de destinatários e percentuais). Outros só permitem cobrança simples e você redistribui depois via TED/PIX próprio.
Como faço reembolso?
PIX reembolso (devolução) é uma operação separada — você chama endpoint /devolucao do gateway com txid e valor. Avise o cliente pelo WhatsApp também ("PIX devolvido. Cair na conta em até 60s").
Próximo passo
Crie sua conta — instâncias permitem envio de imagem, áudio, vídeo, documentos e botões. Criar conta grátis.