Operación

Recordatorio de boleto vencido en WhatsApp: receta con webhook, cola y opt-out

El cobro vencido es el tipo de mensaje en el que un error sale caro: avisar a quien ya pagó irrita, insistir de más termina en queja y el tono equivocado se vuelve un problema jurídico. Esta receta arma un recordatorio defensivo: se dispara con el evento de cobro vencido de Asaas, verifica el estado justo antes de cada envío, manda como máximo dos mensajes y respeta a quien pida parar.

Publicado el · 5 min de lectura

Este artículo también está disponible en: Português · English

Diseña la cadencia antes de escribir código

Define la regla en una frase: el día en que el cobro vence, un recordatorio de ayuda; tres días después, si sigue abierto, un segundo; después de eso, nada por WhatsApp. Dos mensajes bastan para quien se olvidó, y quien no responde a dos probablemente necesita otro canal (llamada, correo, negociación). Más que eso aumenta las denuncias y el riesgo de limitación del número.

Regla de oro del tono: recordatorio, no cobro agresivo. El Código de Defensa del Consumidor de Brasil (art. 42) prohíbe exponer al consumidor moroso al ridículo o a la humillación, así que nada de textos que acusen, amenacen o hablen de reportes a burós de crédito. Si tu operación tiene deuda antigua, trátala con tu asesoría jurídica; esta receta es para atrasos de días.

El disparador: PAYMENT_OVERDUE en Asaas

Asaas envía PAYMENT_OVERDUE cuando el cobro vence. Configura el webhook con ese evento y un authToken (de 32 a 255 caracteres, sin espacios), y valida el encabezado asaas-access-token en cada solicitud. Hay otro evento que parece útil y no lo es: PAYMENT_BANK_SLIP_CANCELLED solo indica que el registro del boleto fue cancelado después del vencimiento, y no significa que el cobro fue eliminado.

Recuerda que la entrega es al menos una vez: guarda el id del evento con restricción de unicidad antes de programar cualquier cosa, como en la receta de Pix pagado. La página de Asaas trae la tabla de eventos y las reglas de la cola pausada.

El endpoint y el worker que reconfirma antes de enviar

El punto central de la receta está en el worker: nunca confía en lo que el evento dijo horas o días atrás. Antes de enviar, consulta el cobro en Asaas y solo continúa si el estado sigue siendo OVERDUE. El cliente que pagó entre el evento y el recordatorio no recibe nada. Revisa en la referencia de Asaas los valores de estado de tu versión de la API antes de pasar a producción.

La programación (la cola 'schedule') es tuya: puede ser una tabla con hora de ejecución y un cron cada minuto, una cola como BullMQ o un flujo en n8n con un nodo Wait. El ejemplo de abajo muestra la lógica, sin atarla a una herramienta.

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()));
}

Cómo parar: un opt-out que funciona

Todos los mensajes del ejemplo terminan con 'responde PARAR'. Eso solo vale si alguien lee la respuesta. Registra el webhook de mensajes recibidos de la instancia (cómo configurarlo), descarta tus propios mensajes (campo fromMe) y, cuando el texto sea PARAR, guarda al cliente en una lista de supresión que la función isOptedOut consulte. Sin esa lista, el aviso de parar es decoración, y es lo primero que un cliente irritado va a probar.

Guarda también el motivo y la fecha de cada envío. Si algún día necesitas explicar por qué un cliente recibió un mensaje, el registro es tu defensa. La guía de LGPD en WhatsApp lista qué registrar.

Variaciones para Kiwify y Eduzz

En Kiwify, el boleto generado se convierte en el evento boleto_gerado (webhook_event_type billet_created), y el pago en compra_aprovada. Kiwify no tiene 'boleto vencido' como disparador: lo habitual es esperar un tiempo después del boleto generado y comprobar si la compra fue aprobada, que es exactamente lo que hace el flujo del asistente con el segundo recordatorio de 1 hora. Mira la página de Kiwify.

En Eduzz, myeduzz.invoice_expired indica factura expirada y el payload de las facturas abiertas trae bankslipUrl y saleRecoveryUrl, que pueden entrar en el mensaje. La página de Eduzz explica el x-signature. En ambas, el teléfono viene en el payload, y el asistente Recuperar vendas cubre los casos de Pix y boleto sin código.

Lista de verificación antes de activar

Prueba con tu número y con un cobro de Sandbox. Confirma que quien paga entre los dos recordatorios no recibe el segundo. Confirma que PARAR realmente bloquea. Confirma que el texto no amenaza nada. Y recuerda que ZapZap conecta números por un canal no oficial y gestionado: no hay ajuste que garantice la ausencia de bloqueo, y la combinación de pocos mensajes, tono respetuoso y lista limpia es lo que reduce el riesgo.

Resumen rápido

  • Dos recordatorios como máximo, y el worker reconfirma el estado del cobro antes de cada envío.
  • PAYMENT_OVERDUE dispara la cadencia; PAYMENT_BANK_SLIP_CANCELLED no significa cobro eliminado.
  • PARAR solo funciona con un webhook de mensajes recibidos y una lista de supresión consultada antes de enviar.
  • Tono de ayuda, sin amenaza ni humillación (art. 42 del CDC de Brasil).

Lee también

Preguntas frecuentes

¿Cuántos mensajes enviar por un cobro vencido?

Dos suelen bastar: uno el día del vencimiento y otro unos días después, si sigue abierto. Pasar de eso aumenta las quejas y el riesgo de limitación del número. Para atrasos largos, usa otro canal.

¿Por qué verificar el estado antes de cada envío?

Porque el evento es una foto del momento en que ocurrió. El cliente pudo haber pagado horas después, y enviar un recordatorio a quien ya pagó es el error más irritante de este mensaje. El worker consulta el cobro y solo envía si sigue vencido.

¿Qué dice el Código de Defensa del Consumidor sobre cobrar?

El artículo 42 prohíbe exponer al consumidor moroso al ridículo y a cualquier tipo de humillación o amenaza. En la práctica: mensaje de recordatorio, sin acusaciones, sin exposición ante terceros. Para dudas jurídicas, consulta a un abogado.

¿Kiwify tiene un evento de boleto vencido?

No entre los disparadores de webhook que revisamos. Existen boleto_gerado y compra_aprovada. El diseño habitual es esperar después del boleto generado y comprobar si la compra fue aprobada.

¿Cómo respeto a quien pide parar?

Recibe los mensajes de la instancia por webhook, ignora los tuyos (fromMe) y, cuando el cliente escriba PARAR, guárdalo en una lista de supresión que el envío consulte siempre. Sin esa consulta, el aviso de parar no tiene efecto.

¿Esto puede bloquear mi número?

Puede. ZapZap usa un canal no oficial, contrario a los términos de WhatsApp, y ningún recurso elimina el riesgo. Pocos mensajes, tono respetuoso, destinatarios que tienen relación contigo y lista limpia reducen la probabilidad, pero no la anulan.

Cobra sin dañar la relación

Arma el recordatorio con pocos mensajes, estado verificado y opción de parar. R$15 por número activo al mes, con crédito de bienvenida sujeto a la campaña vigente.