Cobros, Pix y boleto

WhatsApp con el webhook de Asaas: avisa cuando se pague el Pix o venza el cobro

Asaas envía un POST a tu URL con cada cambio de cobro, con el nombre del evento en event y el objeto payment. Ese payload trae el id del cliente (payment.customer), pero no su celular. Por eso la conexión con WhatsApp pasa por un paso tuyo: recibir el evento, buscar al cliente en Asaas y llamar a la API de ZapZap. Esta página muestra ese camino completo, con código, y dice cuándo no conviene.

Actualizado el

Por qué conectar

Quien cobra por Pix, boleto o tarjeta en Asaas pierde tiempo con dos preguntas repetidas: el cliente dice que pagó y tú no lo ves, o se te olvida que el cobro venció. Con el webhook activo, el pago confirmado genera un mensaje de recibo en segundos y el cobro vencido genera un recordatorio el mismo día, sin que nadie abra el panel. WhatsApp sirve para avisar a quien ya es cliente: escribe solo a quien tiene relación contigo y deja claro cómo dejar de recibir mensajes. ZapZap conecta números por un canal no oficial y gestionado, así que el ritmo y el consentimiento cuentan. Son R$15 por número activo al mes, prepago, y empiezas con crédito de bienvenida al crear la cuenta, sujeto a la campaña vigente.

Paso a paso

  1. 1

    Crea el webhook en Asaas con token de autenticación

    En Asaas, crea un webhook desde la aplicación web o por la API (hasta 10 por cuenta) apuntando a tu endpoint HTTPS, y marca solo los eventos que vayas a usar, por ejemplo PAYMENT_RECEIVED y PAYMENT_OVERDUE. Define el authToken: Asaas lo envía en todas las notificaciones en el encabezado asaas-access-token. Debe tener entre 32 y 255 caracteres, sin espacios, y no puede ser una API Key de Asaas. Prueba primero en el Sandbox: las configuraciones de Sandbox y Producción son independientes.

  2. 2

    Valida el token y guarda el evento antes de responder

    En el endpoint, compara el encabezado asaas-access-token con tu token y rechaza (401) si no coincide. Luego guarda el id del evento en una tabla con restricción de unicidad y solo entonces responde HTTP 200. Asaas entrega cada evento al menos una vez, así que el mismo id puede llegar repetido, y el id es tu clave de idempotencia. Una respuesta lenta provoca timeout, por eso el envío por WhatsApp va después del 200.

  3. 3

    Busca el celular del cliente

    El payload trae payment.customer (algo como cus_000005913252), no el teléfono. Haz GET https://api.asaas.com/v3/customers/{id} con el encabezado access_token y lee mobilePhone (celular) o phone (fijo o general). Si ambos vienen vacíos, no hay a quién escribir: regístralo y sigue. Normaliza a dígitos y agrega el 55 de Brasil si falta.

  4. 4

    Envía el mensaje por la API de ZapZap

    Llama a POST https://api.zapzapapi.com/api/v1/{instanceId}/send/text con x-api-key, x-api-secret y el JSON con number y text. Arma el texto con el nombre, el valor (payment.value) y el enlace del cobro, si lo guardas. Trata 429 y 5xx con reintentos espaciados, y 400, 401 y 403 como error de configuración, sin repetir.

  5. 5

    Reactiva la cola si se pausa

    Después de 15 fallas consecutivas de entrega, Asaas interrumpe la cola de ese webhook, guarda los eventos pendientes hasta 14 días y avisa por correo. Corrige el endpoint, reactiva la cola en el panel de Asaas y revisa en Integraciones > Logs de Webhooks el código HTTP y los intentos. Conviene tener una alerta propia para errores 5xx del endpoint, porque nadie revisa el correo de Asaas todos los días.

Ejemplo

// Express: recibe el evento de Asaas y avisa al cliente por WhatsApp
import express from 'express';
const app = express();

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;

  // Idempotencia: INSERT con UNIQUE en el id del evento. Si ya existe, solo confirma.
  const novo = await salvarEventoUnico(id, req.body); // tu función
  res.sendStatus(200); // responde 200 justo después de guardar
  if (!novo || event !== 'PAYMENT_RECEIVED') return;

  const cli = await fetch('https://api.asaas.com/v3/customers/' + payment.customer, {
    headers: { access_token: process.env.ASAAS_API_KEY },
  }).then((r) => r.json());
  const fone = String(cli.mobilePhone || cli.phone || '').replace(/\D/g, '');
  if (!fone) return;

  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: fone.startsWith('55') ? fone : '55' + fone,
      text: '¡Hola, ' + cli.name + '! Recibimos tu pago de R$ ' + payment.value + '. ¡Gracias!',
    }),
  });
});

Ilustrativo. Los campos y endpoints exactos están en la documentación.

Qué eventos de Asaas valen un mensaje

La lista de eventos de cobro de Asaas es larga (reembolsos, chargeback, Split, negativación). Para el día a día del cobro bastan cuatro, y lo importante es elegir uno por objetivo para no enviar dos mensajes sobre el mismo hecho.

EventoCuándo ocurre (según Asaas)Uso con WhatsApp
PAYMENT_CREATEDGeneración de un nuevo cobro.Aviso de cobro nuevo, si el cliente lo espera.
PAYMENT_CONFIRMEDPago efectuado, pero con el saldo aún no disponible.Tarjeta: primera señal de pago. Usa este o RECEIVED, no los dos.
PAYMENT_RECEIVEDCobro recibido, con el valor disponible en la cuenta Asaas.Recibo y liberación de acceso.
PAYMENT_OVERDUECobro vencido.Recordatorio de vencimiento, con tono de ayuda.

Seguridad, idempotencia y lo que Asaas te pide

  • Autenticación: Asaas no firma el cuerpo con HMAC. Lo que existe es el authToken, enviado en el encabezado asaas-access-token. Valídalo siempre, y si tu infraestructura filtra por origen, usa la lista de IPs oficiales de Asaas.
  • Idempotencia: el id del evento es único y se repite en las reentregas. Guárdalo con restricción de unicidad antes de procesar, y usa también payment.id para no avisar dos veces el mismo pago.
  • Respuesta: devuelve HTTP 200. La página de cola pausada dice que solo el 200 cuenta como éxito, mientras que la de penalización cita cualquier 2xx; usa 200 y quédate del lado seguro.
  • Reintentos: la primera falla reintenta a los 30 segundos, luego 1 minuto, 3 min 30 s y así sucesivamente, con intervalos que llegan a 3 horas. En la falla consecutiva número 15 la cola se pausa.
  • Reaprovechamiento: un webhook de Asaas, un endpoint tuyo. Dale una cola interna y procesa el envío por WhatsApp en segundo plano.

Cuándo NO usar este camino

Si tu objetivo es recuperar carritos abandonados de un infoproducto, Asaas no es el lugar: cobra, no monitorea el checkout. En ese caso mira Kiwify, Hotmart o Eduzz, que tienen evento de carrito y ya entran en el asistente de Recuperación de Ventas de ZapZap sin código. Si no tienes quien mantenga un endpoint, usa n8n con un nodo Webhook y un nodo HTTP Request, que hace el mismo camino sin servidor propio. Y no uses WhatsApp para cobrar a quien nunca habló contigo: sin relación previa y sin consentimiento, el riesgo de denuncia y de bloqueo del número es alto, y la LGPD (la ley de protección de datos de Brasil) exige una base legal para el contacto.

Fuentes y fecha de verificación

Los eventos, la estructura del payload, la idempotencia, el encabezado asaas-access-token, las reglas del token, los reintentos y la cola pausada se verificaron el 29 de octubre de 2026 en la documentación oficial de Asaas (docs.asaas.com), en las páginas Eventos para cobranças, Receba eventos do Asaas no seu endpoint de Webhook, Como implementar idempotência em Webhooks, Fila pausada y Penalização de filas. El campo mobilePhone viene de la referencia Recuperar um único cliente. La documentación cambia: revisa las páginas antes de pasar a producción.

Preguntas frecuentes

¿Asaas envía el teléfono del cliente en el webhook?

No. El objeto payment trae el id del cliente en payment.customer. El celular lo buscas con GET /v3/customers/{id}, en el campo mobilePhone o phone. Por eso la conexión con ZapZap pasa por un endpoint o por un flujo en n8n, y no por el disparador de webhook directo de ZapZap.

¿Cómo sé que el webhook realmente vino de Asaas?

Configura el authToken en el webhook y compáralo con el encabezado asaas-access-token en cada solicitud. No es una firma HMAC del cuerpo, es un token fijo, así que guárdalo como secreto y nunca uses una API Key de Asaas en su lugar. Si restringes por origen, usa las IPs oficiales que publica Asaas.

Recibí el mismo evento dos veces. ¿Es un error?

No. La entrega es al menos una vez, y el mismo id puede llegar repetido. Guarda el id del evento con restricción de unicidad e ignora lo que ya se procesó. Sin eso, el cliente recibe el mismo recibo dos veces.

¿Qué pasa si mi servidor se cae?

Asaas reintenta con intervalos crecientes. Después de 15 fallas consecutivas pausa la cola de ese webhook y guarda los eventos hasta 14 días. Corrige el endpoint y reactiva la cola en el panel; pasado ese plazo, los eventos más antiguos se eliminan.

¿Puedo usar el Sandbox para probar?

Puedes y debes. Las configuraciones de Sandbox y Producción son independientes, así que repite el registro del webhook en producción después de validar el flujo. Del lado de ZapZap, prueba con tu propio número antes de escribir a clientes.

¿Qué evento uso para avisar que el Pix fue pagado?

PAYMENT_RECEIVED indica cobro recibido con el valor disponible en la cuenta. PAYMENT_CONFIRMED indica pago efectuado con el saldo aún no disponible. Elige uno de los dos para el mensaje y usa payment.id para no avisar dos veces.

¿Puedo mandar cobros por WhatsApp a cualquier cliente?

Solo a quien tiene relación contigo y espera ese contacto. Usa un tono de recordatorio, sin presión, y ofrece una forma de dejar de recibir mensajes. ZapZap es un canal no oficial y no elimina el riesgo de bloqueo; el consentimiento, el ritmo humano y la lista limpia siguen siendo tuyos.

Empieza ahora

Crea tu cuenta, conecta un número en minutos y prueba la API con calentamiento y anti-bloqueo. R$15 por número activo/mes, prepago, sin permanencia.