Boleto vencido raramente é calote. Na maior parte das vezes é esquecimento: o e-mail caiu na promoções, o financeiro do cliente não abriu, o PDF ficou na aba 47. Quem opera cobrança recorrente sabe que a diferença entre 8% e 3% de inadimplência quase nunca está na régua de negociação — está no lembrete que chegou onde a pessoa lê.
E o lugar onde a pessoa lê é o WhatsApp. O problema é que cobrança por WhatsApp é o caminho mais rápido para tomar bloqueio, se você fizer errado: disparo em massa, número frio, texto idêntico para mil pessoas. Este post é o desenho de uma régua que funciona sem cair nessa armadilha, com o código que a executa.
O cronograma que funciona
Menos toques, mais previsibilidade. A régua abaixo é a que mais aparece em operações de assinatura e serviço no Brasil:
| Momento | Mensagem | Objetivo |
|---|---|---|
| D-3 | Aviso de vencimento + Pix copia e cola | Antecipar o pagamento |
| D-0 | “Vence hoje” + PDF do boleto | Recuperar quem esqueceu |
| D+2 | Lembrete cordial, sem ameaça | Trazer quem só atrasou |
| D+7 | Aviso de suspensão + link para negociar | Última tentativa automática |
| D+15 | Nada. Humano assume. | Evitar desgaste de marca |
Depois de D+7 o retorno de mensagem automática despenca e o risco de denúncia sobe. Régua boa termina cedo e entrega o caso para uma pessoa.
O envio, em Node.js
A base é uma chamada REST simples. Instância e token vão no path, o corpo tem só telefone e mensagem:
const BASE = 'https://api.wpp-api.com';
const { INSTANCE_ID, TOKEN } = process.env;
async function sendText(phone, message) {
const res = await fetch(
`${BASE}/instances/${INSTANCE_ID}/token/${TOKEN}/send-text`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, message }),
},
);
if (!res.ok) throw new Error(`envio falhou: ${res.status}`);
return res.json(); // { success: true, result: { ... } }
}
Agora a régua propriamente dita. Três detalhes importam mais que o resto: intervalo aleatório entre envios, texto variado e idempotência (nunca mandar o mesmo lembrete duas vezes, nem que o cron rode duas vezes).
const saudacoes = ['Oi', 'Olá', 'Bom dia'];
const pick = (a) => a[Math.floor(Math.random() * a.length)];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function textoD3({ nome, valor, vencimento, pixCopiaECola }) {
return `${pick(saudacoes)}, ${nome}! Sua fatura de ${valor} vence em ${vencimento}.
Se preferir adiantar, o Pix copia e cola está aqui:
${pixCopiaECola}
Qualquer dúvida, é só responder por aqui.`;
}
async function rodarRegua(faturas) {
for (const f of faturas) {
if (await jaEnviado(f.id, 'D-3')) continue; // idempotência
await sendText(f.telefone, textoD3(f));
await marcarEnviado(f.id, 'D-3');
await sleep(8000 + Math.random() * 12000); // 8s a 20s entre contatos
}
}
Esse sleep parece detalhe bobo e não é. Trezentas mensagens idênticas em sessenta segundos é a assinatura de comportamento que derruba número. Espalhar os envios em uma janela de uma ou duas horas custa nada e muda tudo — o assunto está detalhado em como evitar banimento com API não oficial.
Antes do primeiro disparo do mês, vale validar a base: existe GET .../check-number?phone= para conferir se o número tem WhatsApp ativo, e limpar a lista antes evita uma sequência de falhas em cadeia.
Anexar o boleto no D-0
No dia do vencimento, mande o PDF junto. O endpoint de documento aceita URL pública ou base64:
await fetch(`${BASE}/instances/${INSTANCE_ID}/token/${TOKEN}/send-file`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: fatura.telefone,
document: fatura.urlBoletoPdf, // https://... ou base64
fileName: `boleto-${fatura.id}.pdf`,
caption: 'Segue o boleto que vence hoje. O Pix acima também está válido.',
}),
});
Se o boleto fica atrás de autenticação, gere uma URL assinada de curta duração em vez de mandar base64 — arquivo grande deixa o envio lento e o limite de mídia é 100 MB por mensagem.
A parte que quase todo mundo esquece: a resposta
Metade do valor da régua está no que volta. “Já paguei ontem”, “consegue mudar a data?”, “não sou mais cliente” — se isso cai num número que ninguém lê, você acabou de piorar a experiência de quem estava em dia.
Configure o webhook e trate o evento received:
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const assinatura = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body) // corpo bruto, antes do JSON.parse
.digest('hex');
if (assinatura !== req.header('X-WPPAPI-Signature')) {
return res.sendStatus(401);
}
const evento = JSON.parse(req.body);
if (evento.event === 'received' && !evento.data.fromMe) {
filaDeAtendimento.push({
telefone: evento.data.from,
texto: evento.data.body,
});
}
res.sendStatus(200); // responda rápido; processe depois
},
);
Responder 200 na hora e processar em fila importa: webhook lento vira retry, e retry vira mensagem duplicada no seu CRM. Na WPPAPI as entregas com falha são reenviadas e o que esgota as tentativas cai numa dead-letter queue, então nada some silenciosamente — a diferença prática em relação a Z-API e Evolution API está justamente aí, e o guia de webhooks em Node.js cobre o resto do fluxo.
Se você prefere não escrever backend, dá para montar a mesma régua em nodes visuais com o n8n: um Schedule Trigger, um node do seu ERP e o node da WPPAPI fecham o ciclo.
O que não fazer
Três limites honestos, porque cobrança é a área onde erro vira processo:
- Só quem consentiu. Cobrança é comunicação esperada de uma relação contratual, mas o opt-in no canal precisa existir — deixe registrado no cadastro e ofereça saída em toda régua.
- Sem valor devido em grupo, nunca. Expor inadimplência a terceiros é problema de LGPD e de imagem.
- Sem tom de ameaça. “Seu nome será negativado hoje” em mensagem automática é o gatilho número um de denúncia. Informe a consequência com data e sem adjetivo.
E o limite do próprio canal: isso roda em API não oficial, conectada por QR Code. É mais barato e mais rápido de subir que a Cloud API, mas quem trata a base como lista de disparo perde o número. Régua com volume previsível, texto variado e resposta atendida convive bem com a plataforma há anos.
Quer testar com sua base real antes de decidir? O trial de 3 dias não pede cartão — dá para rodar a régua de um dia inteiro e medir a taxa de resposta antes de migrar qualquer coisa.