Operação

Lembrete de boleto vencido no WhatsApp: receita com webhook, fila e opt-out

Cobrança vencida é o tipo de mensagem em que um erro custa caro: avisar quem já pagou irrita, insistir demais vira reclamação e o tom errado vira problema jurídico. Esta receita monta um lembrete defensivo: dispara a partir do evento de cobrança vencida do Asaas, confere o status logo antes de cada envio, manda no máximo duas mensagens e respeita quem pedir para parar.

Publicado em · 4 min de leitura

Este artigo também está disponível em: English · Español

Desenhe a cadência antes de escrever código

Defina a regra em uma frase: no dia em que a cobrança vence, um lembrete de ajuda; três dias depois, se continuar em aberto, um segundo; depois disso, nada pelo WhatsApp. Duas mensagens bastam para quem esqueceu, e quem não responde a duas provavelmente precisa de outro canal (ligação, e-mail, negociação). Mais que isso aumenta denúncia e o risco de limitação do número.

Regra de ouro do tom: lembrete, não cobrança agressiva. O Código de Defesa do Consumidor (art. 42) veda expor o consumidor inadimplente a ridículo ou constrangimento, então nada de texto que acuse, ameace ou fale de negativação. Se a sua operação tem dívida antiga, trate com a sua assessoria jurídica; esta receita é para o atraso de dias.

O gatilho: PAYMENT_OVERDUE no Asaas

O Asaas envia PAYMENT_OVERDUE quando a cobrança vence. Configure o webhook com esse evento e um authToken (32 a 255 caracteres, sem espaços), e valide o cabeçalho asaas-access-token em toda requisição. Há outro evento que parece útil e não é: PAYMENT_BANK_SLIP_CANCELLED só indica que o registro do boleto foi cancelado depois do vencimento, e não significa que a cobrança foi removida.

Lembre-se de que a entrega é pelo menos uma vez: grave o id do evento com restrição de unicidade antes de agendar qualquer coisa, como na receita de Pix pago. A página Asaas traz a tabela de eventos e as regras de fila pausada.

O endpoint e o worker que reconfere antes de enviar

O ponto central da receita está no worker: ele nunca confia no que o evento disse horas ou dias atrás. Antes de enviar, consulta a cobrança no Asaas e só segue se o status ainda for OVERDUE. Cliente que pagou entre o evento e o lembrete não recebe nada. Confira na referência do Asaas os valores de status da sua versão da API antes de ir para produção.

O agendamento (a fila 'schedule') é seu: pode ser uma tabela com horário de execução e um cron a cada minuto, uma fila como BullMQ ou um fluxo no n8n com um nó Wait. O exemplo abaixo mostra a lógica, sem amarrar a uma ferramenta.

Node.js (Express)

// POST /asaas : first reminder when a charge becomes overdue, second one 3 days later if still unpaid
app.post("/asaas", express.json(), async (req, res) => {
  if (req.get("asaas-access-token") !== process.env.ASAAS_WEBHOOK_TOKEN) return res.sendStatus(401);
  const { id, event, payment } = req.body;
  const fresh = await saveEventOnce(id, req.body); // INSERT ... ON CONFLICT DO NOTHING
  res.sendStatus(200);
  if (!fresh || event !== "PAYMENT_OVERDUE") return;

  if (await isOptedOut(payment.customer)) return;   // suppression list, see the opt-out section
  await schedule("reminder-1", payment.id, 0);      // your queue: run now
  await schedule("reminder-2", payment.id, 3 * 24 * 3600); // run in 3 days
});

// Worker: always re-check the charge right before sending
async function runReminder(kind, paymentId) {
  const p = await fetch("https://api.asaas.com/v3/payments/" + paymentId, {
    headers: { access_token: process.env.ASAAS_API_KEY },
  }).then((r) => r.json());
  if (p.status !== "OVERDUE") return;               // paid, canceled or deleted meanwhile: say nothing
  const c = await fetch("https://api.asaas.com/v3/customers/" + p.customer, {
    headers: { access_token: process.env.ASAAS_API_KEY },
  }).then((r) => r.json());
  const phone = String(c.mobilePhone || c.phone || "").replace(/\D/g, "");
  if (phone.length < 10 || (await isOptedOut(p.customer))) return;
  const first = c.name.split(" ")[0];
  const text = kind === "reminder-1"
    ? "Oi, " + first + ". Sua cobrança venceu e ainda consta em aberto. Se já pagou, pode ignorar. Se precisar de uma segunda via ou de outra data, é só responder aqui. Para não receber esses avisos, responda PARAR."
    : first + ", passando só para confirmar se deu tudo certo com a sua cobrança. Posso ajudar com a segunda via? Para não receber mais avisos, responda PARAR.";
  await sendWhatsApp(phone.startsWith("55") ? phone : "55" + phone, text);
}

// Shared helper: one send through the ZapZap API
async function sendWhatsApp(number, text) {
  const r = await fetch(`https://api.zapzapapi.com/api/v1/${process.env.ZAPZAP_INSTANCE}/send/text`, {
    method: "POST",
    headers: {
      "x-api-key": process.env.ZAPZAP_API_KEY,
      "x-api-secret": process.env.ZAPZAP_API_SECRET,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ number, text }),
  });
  if (!r.ok) throw new Error("ZapZap " + r.status + ": " + (await r.text()));
}

Como parar: opt-out que funciona

Toda mensagem do exemplo termina com 'responda PARAR'. Isso só vale se alguém lê a resposta. Registre o webhook de mensagens recebidas da instância (como configurar), descarte as suas próprias mensagens (campo fromMe) e, quando o texto for PARAR, grave o cliente numa lista de supressão que a função isOptedOut consulta. Sem essa lista, o botão de parar é decoração, e é a primeira coisa que um cliente irritado vai testar.

Guarde também o motivo e a data de cada envio. Se um dia você precisar explicar por que um cliente recebeu uma mensagem, o log é a sua defesa. O guia de LGPD no WhatsApp lista o que registrar.

Variações para Kiwify e Eduzz

Na Kiwify, o boleto gerado vira o evento boleto_gerado (webhook_event_type billet_created), e o pagamento vira compra_aprovada. A Kiwify não tem 'boleto vencido' como gatilho: o padrão é esperar um tempo depois do boleto gerado e conferir se a compra foi aprovada, que é exatamente o que o fluxo do assistente faz com o segundo lembrete de 1 hora. Veja a página Kiwify.

Na Eduzz, myeduzz.invoice_expired indica fatura expirada e o payload das faturas em aberto traz bankslipUrl e saleRecoveryUrl, que podem entrar na mensagem. A página Eduzz explica o x-signature. Em ambos, o telefone vem no payload, e o assistente Recuperar vendas cobre os casos de Pix e boleto sem código.

Checklist antes de ligar

Teste com o seu número e com uma cobrança de Sandbox. Confirme que quem paga entre os dois lembretes não recebe o segundo. Confirme que PARAR realmente bloqueia. Confirme que o texto não ameaça nada. E lembre que a ZapZap conecta números por um canal não oficial e gerenciado: não há ajuste que garanta ausência de bloqueio, e a combinação de poucas mensagens, tom respeitoso e lista limpa é o que reduz o risco.

Resumo rápido

  • No máximo dois lembretes, e o worker reconfere o status da cobrança antes de cada envio.
  • PAYMENT_OVERDUE dispara a cadência; PAYMENT_BANK_SLIP_CANCELLED não significa cobrança removida.
  • PARAR só funciona com webhook de mensagens recebidas e uma lista de supressão consultada antes de enviar.
  • Tom de ajuda, sem ameaça nem constrangimento (art. 42 do CDC).

Perguntas frequentes

Quantas mensagens mandar para uma cobrança vencida?

Duas costumam bastar: uma no dia do vencimento e outra alguns dias depois, se continuar em aberto. Passar disso aumenta reclamação e o risco de limitação do número. Para atraso longo, use outro canal.

Por que checar o status antes de cada envio?

Porque o evento é um retrato de quando ocorreu. O cliente pode ter pago horas depois, e mandar lembrete a quem já pagou é o erro mais irritante dessa mensagem. O worker consulta a cobrança e só envia se ela ainda estiver vencida.

O que o Código de Defesa do Consumidor diz sobre cobrar?

O artigo 42 proíbe expor o consumidor inadimplente a ridículo e a qualquer tipo de constrangimento ou ameaça. Em termos práticos: mensagem de lembrete, sem acusação, sem exposição a terceiros. Para dúvidas jurídicas, consulte um advogado.

A Kiwify tem evento de boleto vencido?

Não entre os gatilhos de webhook que conferimos. Existem boleto_gerado e compra_aprovada. O desenho usual é esperar após o boleto gerado e conferir se a compra foi aprovada.

Como respeitar quem pede para parar?

Receba as mensagens da instância por webhook, ignore as suas (fromMe) e, quando o cliente escrever PARAR, grave-o numa lista de supressão que o envio consulta sempre. Sem essa consulta, o aviso de parar não tem efeito.

Isso pode bloquear meu número?

Pode. A ZapZap usa um canal não oficial, contra os termos do WhatsApp, e nenhum recurso elimina o risco. Poucas mensagens, tom respeitoso, destinatário que tem relação com você e lista limpa reduzem a chance, mas não a zeram.

Cobre sem queimar a relação

Monte o lembrete com poucas mensagens, status conferido e opção de parar. R$15 por número ativo ao mês, com crédito de boas-vindas sujeito à campanha vigente.