Ir al contenido

Webhooks firmados

En lugar de consultar la API en bucle, deja que WhatSetter te empuje los eventos a tu servidor en cuanto suceden. Cada entrega va firmada, así que puedes estar seguro de que viene de nosotros.

Evento Se dispara cuando Estado
contact.qualified El agente califica un lead (según los criterios de tu prompt) Activo
contact.not_qualified El agente descarta un lead Activo
agent.disconnected Un número de WhatsApp se desconecta Activo
booking.created Se agenda una reunión en la conversación Aún no se emite
message.received Un lead responde Aún no se emite

POST /webhooks necesita el scope webhooks:manage. La URL debe ser HTTPS (HTTP plano solo se acepta para localhost).

Terminal window
curl -X POST https://app.whatsetter.com/api/v1/webhooks \
-H "Authorization: Bearer $WHATSETTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://tu-servidor.com/webhooks/whatsetter","events":["contact.qualified","contact.not_qualified"],"description":"Sync CRM"}'
{
"data": {
"id": "66f1c0ffee00000000000abc",
"url": "https://tu-servidor.com/webhooks/whatsetter",
"events": ["contact.qualified", "contact.not_qualified"],
"active": true,
"description": "Sync CRM",
"secret_prefix": "whsec_3f9a",
"secret": "whsec_3f9a…",
"last_delivery_at": null,
"last_delivery_status": null,
"created_at": "2026-09-19T10:00:00.000Z"
}
}

La respuesta contiene el secreto de firma (whsec_…). Solo se muestra aquí: guárdalo de inmediato. Las llamadas posteriores a GET /webhooks solo devuelven secret_prefix.

Errores que importan:

HTTP code Por qué
422 validation_error url ausente o sin HTTPS, events vacío o desconocido, description de más de 255 caracteres.
429 quota_exceeded El espacio ya tiene 10 suscripciones. Borra una primero.

POST /webhooks/{id}/test envía de inmediato un evento de ejemplo firmado a la URL de la suscripción e informa de cómo respondió tu endpoint. El ejemplo usa el primer evento de la suscripción y lleva "is_test": true.

Terminal window
curl -X POST https://app.whatsetter.com/api/v1/webhooks/66f1c0ffee00000000000abc/test \
-H "Authorization: Bearer $WHATSETTER_API_KEY"
{
"data": {
"delivered": true,
"endpoint_status": 200,
"error": null,
"event_id": "evt_9f2c1a7e4b8d3056a1cf7e2b9d4a6013"
}
}

delivered es false cuando tu endpoint respondió algo distinto de un estado 2xx (mira endpoint_status) o no se pudo alcanzar en 10 segundos (mira error). Un 404 not_found significa que el id de suscripción es desconocido o pertenece a otro espacio.

Cada entrega comparte el mismo sobre. Los campos propios del evento van al lado de los del sobre, no anidados bajo una clave payload.

{
"event_type": "contact.qualified",
"event_id": "evt_9f2c1a7e4b8d3056a1cf7e2b9d4a6013",
"api_version": "2026-06-01",
"occurred_at": "2026-09-15T09:10:12.517Z",
"team_id": "6b0c1d2e3f405162738a",
"is_test": false,
"campaign": {
"id": "6b0c1d2e3f4051627abc",
"slug": "acme-morocco-1-ab12",
"name": "Alex"
},
"contact": {
"id": "6b0c1d2e3f4051627def",
"lead_id": "212600000000@c.us",
"name": "Sam",
"phone": "212600000000",
"classification": "qualified",
"ai_reasoning": "Busca un SUV, presupuesto confirmado, quiere que le llamen.",
"conversation_url": "https://app.whatsetter.com/dashboard/conversations?leadId=212600000000%40c.us",
"external_reference": "your-crm-id-12345"
}
}
Campo Detalle
event_type Uno de los eventos de arriba.
event_id Único por entrega. Úsalo para deduplicar.
api_version Actualmente 2026-06-01.
occurred_at ISO 8601, en UTC.
team_id El espacio de WhatSetter al que pertenece el evento.
is_test true si se envió desde POST /webhooks/{id}/test.
Campo Detalle
campaign.id Identificador interno de la campaña.
campaign.slug Identificador estable y legible de la campaña.
campaign.name El nombre visible del agente.
contact.id Identificador interno del lead, el mismo id que en GET /leads.
contact.lead_id Identificador de WhatsApp en bruto, por ejemplo 212600000000@c.us.
contact.name Nombre detectado por el agente, puede ser null.
contact.phone Solo dígitos, mira el aviso de abajo.
contact.classification El veredicto del agente.
contact.ai_reasoning Por qué el agente lo decidió así, en texto claro.
contact.conversation_url Enlace directo a la conversación en WhatSetter.
contact.external_reference Tu propio identificador, mira el consejo de abajo.
{
"event_type": "agent.disconnected",
"event_id": "evt_…",
"api_version": "2026-06-01",
"occurred_at": "2026-09-14T13:00:09.250Z",
"team_id": "6b0c1d2e3f405162738a",
"is_test": false,
"agent": {
"name": "Alex",
"phone": "212611111111@c.us",
"campaign_id": "acme-morocco-1-ab12",
"campaign_name": "Acme Morocco 1",
"session_status": "disconnected",
"disconnected_at": "2026-09-14T12:54:00.000Z"
}
}

Se envía una sola vez, al detectar el incidente. Los recordatorios de reconexión no lo reenvían.

Cada entrega va firmada con HMAC-SHA256 del cuerpo en bruto de la petición, usando el secreto de tu suscripción. Las cabeceras que recibes:

Content-Type: application/json
User-Agent: Whatsetter-Webhooks/1.0
X-Whatsetter-Event: contact.qualified
X-Whatsetter-Delivery: evt_9f2c1a7e4b8d3056a1cf7e2b9d4a6013
X-Whatsetter-Timestamp: 2026-09-15T09:10:12.517Z
X-Whatsetter-Api-Version: 2026-06-01
X-Whatsetter-Signature: sha256=<hex>

X-Whatsetter-Delivery repite el event_id del sobre, y X-Whatsetter-Timestamp su occurred_at. Calcula sha256= + el HMAC hexadecimal del cuerpo en bruto y compáralo con X-Whatsetter-Signature en tiempo constante.

import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody) // el cuerpo EN BRUTO, antes de cualquier JSON.parse
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: conserva el cuerpo en bruto en esta ruta
// app.post('/webhooks/whatsetter', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verify(req.body, req.get('X-Whatsetter-Signature'), process.env.WHATSETTER_WEBHOOK_SECRET)) {
// return res.status(401).end();
// }
// const event = JSON.parse(req.body);
// res.status(200).end(); // responde rápido, procesa en asíncrono
// });
  • Un solo intento por evento. Si tu endpoint está caído o responde un error, el evento no se reintenta y se pierde. Mantén el endpoint disponible, y reconcilia con GET /leads si sospechas un hueco.
  • La petición caduca a los 10 segundos. Responde 2xx de inmediato y haz tu procesamiento de forma asíncrona; un timeout cuenta como fallo.
  • Deduplica igualmente por event_id. No cuesta nada y te protege si más adelante se añaden reintentos.
  • Un evento se reparte a cada suscripción activa que lo incluya, hasta 10 suscripciones por espacio. Dos suscripciones a la misma URL reciben cada una su copia.
  • last_delivery_at y last_delivery_status en la suscripción te dicen cómo fue la última entrega.
  • GET /webhooks lista tus suscripciones, sin el secreto.
  • DELETE /webhooks/{id} detiene las entregas de inmediato. Para cambiar la URL o los eventos, borra y vuelve a crear: obtienes un secreto nuevo.