WPPAPI
Blog

Régua de cobrança no WhatsApp: boleto e Pix sem virar spam

·Equipe WPPAPIcaso de usonode.jscobrançawebhooks

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.

Coloque em prática

3 dias grátis, sem cartão — API, webhooks e Chatwoot inclusos.

Testar 3 dias grátis