Recibir los mensajes de WhatsApp en tu sistema
Cada vez que un contacto le escribe a un número de tu cuenta, tu servidor recibe el mensaje firmado, sin consultar la API en bucle.
Vas a armar un receptor HTTPS que recibe el evento message.received cada vez que un contacto le escribe a uno de tus números de WhatsApp. Usa Crear un webhook para suscribirte, Enviar un evento de prueba para probar el receptor y Listar entregas de un webhook para revisar qué llegó.
Al terminar esta guía, cada mensaje entrante llega a tu servidor como un POST firmado con el texto, el tipo de mensaje, el contacto que escribió y el número de tu cuenta que lo recibió. Tu receptor verifica la firma, responde enseguida y procesa cada mensaje una sola vez.
Antes de empezar
- Una clave con
webhooks:writeyconversations:read.webhooks:writecrea, prueba y lee la suscripción y su historial de entregas;conversations:reades el permiso del área demessage.received, que la suscripción pide para ese evento. Para ver el catálogo de eventos desde la API hace falta ademásaccounts:read. La clave tiene que ser del titular de la cuenta. Si todavía no tenés una, seguí la guía rápida; los permisos están en Autenticación y permisos. - La clave en la variable
FC_API_KEY. Lleva un|: en un archivo.env, escribila entre comillas. - Un servidor con una URL
httpspública. Una dirección interna no recibe entregas.
1. Armá el receptor
Tu receptor tiene que hacer cuatro cosas, en este orden:
- Leer el cuerpo crudo, tal como llegó, sin parsearlo.
- Verificar la firma de la cabecera
X-Funnelchat-Signaturecon el secreto de la suscripción. Si no es válida, responder400y no hacer nada. La fórmula y el código probado en Node.js, Python y PHP están en Verificar la firma: copiá el de tu lenguaje a tu proyecto tal cual (los ejemplos de abajo lo importan comoverificar-firma.jsyverificar_firma.py). - Responder
2xxenseguida. Si no respondés, la entrega cuenta como fallida y se reintenta. Procesá el mensaje después, en segundo plano. - Descartar los repetidos por
id. La entrega es «al menos una vez»: el mismo evento puede llegar más de una vez, siempre con el mismoiden el cuerpo.
Leé type e id del cuerpo ya verificado, no de las cabeceras: solo el cuerpo está firmado.
// receptor.js · Node 18+ con Express
const express = require('express');
const { verificarFirma } = require('./verificar-firma'); // el código de /docs/webhooks/#firma
const app = express();
const procesados = new Set(); // en producción, guardá los id en tu base de datos
app.post('/hooks/funnelchat', express.raw({ type: 'application/json' }), (req, res) => {
const firma = req.get('X-Funnelchat-Signature');
if (!verificarFirma(req.body, firma, process.env.FUNNELCHAT_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const evento = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // primero respondé; después procesá
if (procesados.has(evento.id)) return; // repetido: ya lo procesaste
if (evento.type !== 'message.received') return; // por ejemplo, webhook.test
setImmediate(async () => {
await guardarMensaje(evento.data); // en producción, mandalo a una cola
procesados.add(evento.id);
});
});
async function guardarMensaje({ message, contact, receiver, device_id }) {
console.log(`${contact.phone} le escribió a ${receiver.phone} (número ${device_id}): [${message.type}] ${message.text ?? ''}`);
}
app.listen(3000);FUNNELCHAT_WEBHOOK_SECRET es el secreto de la suscripción que vas a crear en el paso siguiente. Hasta tenerlo, tu receptor rechaza todo con 400, y está bien.
2. Suscribite a message.received
Con el receptor publicado, creá la suscripción. Hay un solo lugar para suscribirse a los eventos de las tres áreas, /api/v1/webhooks. Pide webhooks:write y, por ser un evento de conversations, conversations:read.
curl -X POST "https://api.funnelchat.com/api/v1/webhooks" \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-servidor.example/hooks/funnelchat",
"description": "Mensajes entrantes",
"events": ["message.received"]
}'La respuesta (201) es la suscripción, sin envoltorio data. Estos son sus campos según la especificación; los valores son de ejemplo:
{
"id": "00000000-0000-4000-8000-000000000001",
"url": "https://tu-servidor.example/hooks/funnelchat",
"description": "Mensajes entrantes",
"events": ["message.received"],
"filters": {},
"status": "active",
"disabled_reason": null,
"last_delivery_at": null,
"last_success_at": null,
"secret_rotated_at": null,
"created_at": "2026-10-01T12:00:00+00:00",
"updated_at": "2026-10-01T12:00:00+00:00",
"migration": null,
"secret": "whsec_<secreto de firma>",
"secret_notice": "Guardá este secreto ahora: es la única vez que se muestra. Con él validás la firma del header de cada entrega."
}El secreto se ve una sola vez
Guardá secret en tu servidor, completo y tal como llegó (con el prefijo whsec_), en FUNNELCHAT_WEBHOOK_SECRET: con él se firma cada entrega. No se puede volver a consultar; si lo perdés, rotalo desde el panel de Funnelchat. Guardá también id: identifica tu suscripción entre las de la cuenta (detalle).
3. Probá la entrega
No hace falta esperar a que alguien escriba: Enviar un evento de prueba manda ya un evento webhook.test a tu URL, firmado igual que uno real, y te dice cómo le fue. {id} es el id del paso anterior (en el ejemplo de curl, la variable WEBHOOK_ID). Pide webhooks:write.
curl -X POST "https://api.funnelchat.com/api/v1/webhooks/$WEBHOOK_ID/test" \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Accept: application/json"Respuesta de ejemplo (200, con los campos de la especificación):
{
"succeeded": true,
"response_status": 200,
"error": null,
"duration_ms": 184,
"delivery_id": 9012
}La prueba llegó si succeeded es true. Si no, response_status y error dicen por qué (non_2xx, timeout, connection_failed, tls_error, destination_not_allowed…); el intento queda además en Listar entregas de un webhook, junto con todas las entregas reales. La prueba no se reintenta.
4. Leé el mensaje
Cuando un contacto le escribe a uno de tus números, llega esto (ejemplo del catálogo de eventos):
{
"id": "evt_000000000000000000000001",
"type": "message.received",
"version": 1,
"occurred_at": "2026-10-01T12:00:00Z",
"data": {
"message": {
"id": 1001,
"type": "text",
"text": "Hola, quiero más información",
"media": null
},
"contact": {
"id": 2001,
"name": "Contacto de ejemplo",
"phone": "+5491100000000"
},
"receiver": {
"phone": "+5491100000001"
},
"device_id": 4521
}
}| Campo | Qué es |
|---|---|
id | Identificador del evento. Es el mismo en todos los reintentos: con él descartás repetidos. |
occurred_at | Cuándo ocurrió, en UTC (ISO 8601). |
data.message.type | Tipo de mensaje, como texto: text, image, video, document, sticker, contact, audio, note, action, invalid, interactive, location, voice_call, video_call, event, button, template, notification, option_list, template_button_reply, poll, whatsapp_form, call_permission_request, call_permission_reply o request_contact_info. |
data.message.text | El texto del mensaje. |
data.message.media | null en el ejemplo, que es un mensaje de texto. |
data.contact | Quién escribió: id, name y phone. |
data.receiver.phone | El teléfono del número de tu cuenta que recibió el mensaje. |
data.device_id | El número de tu cuenta que recibió el mensaje: el mismo device_id que mandás para responder por ese número. Es null si el mensaje no tiene número asociado. |
- Los teléfonos vienen con
+y el código de país. La API de contactos los guarda sin+: para buscar al contacto, quitáselo antes (El teléfono en los eventos y en la API). - Tolerá campos y valores nuevos. El catálogo anuncia datos planificados dentro de este evento que el ejemplo de hoy todavía no trae. Si llega algo que no conocés, respondé
2xxigual. - Para saber a cuál de tus números le escribieron, usá
device_id: es elidde Listar números (pideaccounts:read).
- La forma de
mediaen imágenes, audios, documentos y otros archivos: el ejemplo solo muestra un mensaje de texto, conmedia: null. Antes de procesar archivos, mirá entregas reales en tu receptor. - Si
data.contact.idydata.message.idson los mismos identificadores que usa la API de conversations. Para encontrar al contacto en la API, usá el teléfono, como en Leer conversaciones e historial de mensajes.
Si falla
| Qué ves | Qué pasó | Qué hacer |
|---|---|---|
401 | Falta la clave, no existe o fue revocada. | Revisá la cabecera Authorization: Bearer con la clave completa. |
403 con error: "permission_denied" | A la clave le falta un permiso: webhooks:write para crear y probar, webhooks:read para ver la suscripción y sus entregas, o conversations:read, el del área de message.received. | Creá una clave con ese permiso. message dice cuál falta. |
403 sin error | Lo rechazó la autorización propia de la ruta, no el portero. | Leé message. Recordá que las rutas de webhooks piden una clave del titular de la cuenta. |
422 al crear | La validación falló: por ejemplo, url no es una URL válida, events trae un tipo que no existe, o a la clave le falta conversations:read para message.received. | errors dice qué campo corregir. |
404 al probar | No existe una suscripción con ese id en la cuenta. | Revisá el id que guardaste al crearla. |
422 al probar | La suscripción está disabled: no se envía nada. | Mirá disabled_reason en la suscripción (abajo). |
La prueba vuelve con succeeded: false | Tu receptor no respondió 2xx, o no se pudo llegar. | Mirá response_status y error. Un 400 de tu receptor suele ser la firma: cuerpo re-serializado, secreto incompleto o reloj desincronizado más de 300 segundos. |
| No llegan los mensajes reales | La suscripción está pausada o deshabilitada. | Mirá status y disabled_reason. Con key_revoked no se puede reactivar: creá una nueva. Si se deshabilitó por 5 entregas fallidas seguidas, arreglá el receptor y reactivala con PUT y status: "active". |
503 | El portero no pudo evaluar la llamada (gate_unavailable). | Esperá lo que indica Retry-After y reintentá. |
Todos los códigos del portero están en Errores, y los reintentos de las entregas, en Webhooks.
Relacionado
- Leer conversaciones e historial de mensajes: traé la conversación completa del contacto que te escribió.
- Enviar mensajes: cómo responder con una clave con
conversations:sendy las reglas de WhatsApp. Para responder con tus tiempos: Armá tu propio flujo. - Webhooks: la firma, los reintentos y qué hacer en tu receptor.
- Catálogo de eventos: los demás eventos, como
conversation.assigned,contact.stage_changedomessage.status_changed. Se suman a la misma suscripción. - Listar tipos de evento: el catálogo desde la API, con el
statusde cada tipo.