Guía rápida
Crear una clave, hacer la primera llamada, suscribirse a message.received y verificar la firma de cada entrega.
De cero a recibir tu primer evento firmado, en cuatro pasos. Necesitás una cuenta de Funnelchat con acceso a la beta privada de la API y un servidor con una URL https pública para recibir los webhooks.
1. Creá una clave de API
Entrá a la pantalla Desarrolladores de tu cuenta, poné una etiqueta para reconocerla y elegí los permisos. Para esta guía alcanzan conversations:read (leer contactos y elegir eventos de conversations) y webhooks:write (administrar la suscripción). La pantalla está disponible para el titular de la cuenta que esté en la beta privada de la API.
La clave se ve una sola vez
Copiala apenas la crees: tiene la forma <id>|fc_live_… y no se puede volver a consultar. Las claves no vencen: si se filtra, revocala desde la misma pantalla.
Si la guardás en un archivo .env, ponela entre comillas. El | es parte de la clave: sin comillas, al cargar el archivo con source la shell lo toma como una tubería y la variable queda vacía.
FC_API_KEY='<id>|fc_live_…'También se puede crear por API, con tu sesión web de Funnelchat. Una clave nunca puede crear, listar ni revocar claves: esa ruta le responde 403 route_not_available, y por eso no aparece en la referencia.
curl -X POST "https://api.funnelchat.com/api/v1/me/api-keys" \
-H "Authorization: Bearer <credencial de tu sesión web>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"label": "Mi integración",
"abilities": ["conversations:read", "webhooks:write"]
}'label es el nombre de la clave (hasta 120 caracteres) y abilities, la lista de permisos. La respuesta trae la clave completa una sola vez.
2. Hacé tu primera llamada
La API tiene una sola URL base, y la URL de una llamada es esa URL base + la ruta completa, que siempre empieza con /api/v1/ (ver Primeros pasos y Cómo armar la URL). Esta llamada lista tus primeros cinco contactos en conversations:
curl "https://api.funnelchat.com/api/v1/contacts?perPage=5" \
-H "Authorization: Bearer <id>|fc_live_..." \
-H "Accept: application/json"Responde 200 con data, links, meta (según la especificación). Pide el permiso conversations:read.
Si la clave no existe o fue revocada, recibís 401; si le falta el permiso, 403 permission_denied. Todos los códigos están en Autenticación y permisos.
La respuesta
La respuesta viene paginada: data trae los contactos, links los enlaces a otras páginas y meta los totales. Esta es una respuesta real de esta llamada (05-10), recortada a uno de los cinco contactos y a tres de los quince elementos de meta.links:
{
"data": [
{
"id": 102578466,
"name": "Ana Pérez",
"email": "",
"phone": "5491100000000",
"whatsapp_id": "",
"whatsapp_username": null,
"has_phone": true,
"country_code": "",
"status": 1,
"image": "",
"notes": null,
"created_at": "2026-10-04T21:23:45.000000Z",
"active_conversation": 0,
"favorite": 0,
"unread_messages": 0,
"device_id": null,
"adviser_id": null,
"paused_at": null,
"mac_date": null,
"ai_assistant_name": null
}
],
"links": {
"first": "https://api.funnelchat.com/api/v1/contacts?perPage=5&page=1",
"last": "https://api.funnelchat.com/api/v1/contacts?perPage=5&page=88",
"prev": null,
"next": "https://api.funnelchat.com/api/v1/contacts?perPage=5&page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 88,
"links": [
{ "url": null, "label": "« Anterior", "page": null, "active": false },
{ "url": "https://api.funnelchat.com/api/v1/contacts?perPage=5&page=1", "label": "1", "page": 1, "active": true },
{ "url": "https://api.funnelchat.com/api/v1/contacts?perPage=5&page=2", "label": "Siguiente »", "page": 2, "active": false }
],
"path": "https://api.funnelchat.com/api/v1/contacts",
"per_page": 5,
"to": 5,
"total": 436
}
}- Con una clave siempre viene paginada. Sin
perPage, trae 25 contactos por página. - Para la página siguiente, seguí
links.next: conservaperPagey los filtros de tu consulta. Pará cuandolinks.nextseanull. - El teléfono viene sin
+(phone: "5491100000000"), aunque los eventos lo traen con+. Para buscar un contacto por el teléfono de un evento, quitale el+antes: ver El teléfono en los eventos y en la API.
Identificadores internos
Las respuestas de conversations pueden traer user_id, owner_id o device_id. En v0 se aceptan, pero no son estables: no los uses como identificadores de tu cuenta.
3. Suscribite a message.received
Las suscripciones a webhooks se administran en un solo lugar, /api/v1/webhooks, para los eventos de las tres áreas. message.received —un contacto le escribió a un número de WhatsApp de tu cuenta— es del área de conversations, así que la clave necesita webhooks:write y además conversations:read:
curl -X POST "https://api.funnelchat.com/api/v1/webhooks" \
-H "Authorization: Bearer <id>|fc_live_..." \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-servidor.example/hooks/funnelchat",
"events": [
"message.received"
],
"description": "Mi integración"
}'Responde 201 con la suscripción: id, url, description, events, filters, status, disabled_reason, last_delivery_at, last_success_at, secret_rotated_at, created_at, updated_at, migration, secret, secret_notice. Pide el permiso webhooks:write y, para message.received, también conversations:read: cada evento pide el permiso de lectura de su área.
El secreto se ve una sola vez
La respuesta trae secret: guardalo en tu servidor, es con lo que se firma cada entrega. No se puede volver a consultar; si lo perdés, generá otro desde el panel de Funnelchat (la clave no puede rotarlo).
urles la dirección pública de tu receptor; usáhttps.eventsacepta de 1 a 50 tipos del catálogo, de cualquiera de las tres áreas; no hay un comodín para «todos». Elegí eventos constatus: "active"enGET /api/v1/webhooks/events: los planificados se aceptan, pero no llegan hasta que se empiecen a emitir (detalle).- Guardá el
idde la respuesta. Identifica tu suscripción: las suscripciones son de la cuenta yGET /api/v1/webhookstambién muestra las de otras integraciones. Si cambia tu URL, actualizala conPUTen lugar de crear otra (detalle). - Para probar sin esperar un mensaje real,
POST https://api.funnelchat.com/api/v1/webhooks/{id}/testmanda un eventowebhook.testfirmado igual que uno real y te responde si tu servidor contestó2xx(succeeded).{id}es elidde la respuesta del alta.
4. Verificá la firma
Cada entrega es un POST con el sobre {id, type, version, occurred_at, data} y la cabecera X-Funnelchat-Signature: t=<unix>,v1=<hex>. v1 es el HMAC-SHA256, en hexadecimal, de t + "." + cuerpo crudo, con el secreto completo de la suscripción tal como se mostró. Verificala sobre el cuerpo crudo, antes de parsear el JSON:
// Verifica la cabecera X-Funnelchat-Signature de una entrega de webhook (Node.js 16+).
// cuerpoCrudo: el cuerpo tal como llegó (Buffer o string), sin parsear ni volver a serializar.
// secreto: el secreto completo de la suscripción, tal como se mostró al crearla o rotarla.
const crypto = require('node:crypto');
function verificarFirma(cuerpoCrudo, cabecera, secreto, toleranciaSegundos = 300) {
if (typeof cabecera !== 'string' || typeof secreto !== 'string' || secreto === '') return false;
let t = null;
const firmas = [];
for (const parte of cabecera.split(',')) {
const i = parte.indexOf('=');
if (i === -1) continue;
const clave = parte.slice(0, i).trim();
const valor = parte.slice(i + 1).trim();
if (clave === 't') t = valor;
if (clave === 'v1') firmas.push(valor);
}
if (t === null || !/^\d+$/.test(t) || firmas.length === 0) return false;
// Rechazá entregas viejas o del futuro (protección contra repeticiones).
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranciaSegundos) return false;
const esperada = crypto
.createHmac('sha256', secreto)
.update(`${t}.`)
.update(cuerpoCrudo)
.digest('hex');
const a = Buffer.from(esperada, 'utf8');
return firmas.some((v1) => {
const b = Buffer.from(v1, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}
module.exports = { verificarFirma };
// Uso con Express: leé el cuerpo crudo, verificá, y recién después parseá.
// app.post('/hooks/funnelchat', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verificarFirma(req.body, req.get('X-Funnelchat-Signature'), process.env.FUNNELCHAT_WEBHOOK_SECRET)) {
// return res.sendStatus(400);
// }
// const evento = JSON.parse(req.body.toString('utf8')); // tomá type e id de acá, no de las cabeceras
// res.sendStatus(200); // respondé rápido; procesá después
// });Último resultado de tests/test_firmas.py (2026-10-22 20:53 (+00:00)): pasó — Python 3.12.15, Node v24.21.0, PHP 8.3.6.
Después: respondé 2xx enseguida y procesá en segundo plano, y descartá los eventos repetidos por su id. El detalle está en Webhooks.
Y después
- Mirá qué otros eventos podés recibir en el catálogo.
- Explorá las rutas de los tres dominios y probalas con tu clave en la referencia.
- Si programás con una IA, pasale llms-full.txt: tiene todo esto más un resumen de cada endpoint.
Primeros pasos
La URL base única de la API, cómo autenticarte con tu clave, un ejemplo de lista y de paginación, los errores comunes, el id de cada pedido y los límites de llamadas.
Armá tu propio flujo
Un flujo completo con tus tiempos y tu lógica: te llega un mensaje, esperás, decidís, respondés con texto o con una plantilla y te enterás si lo leyeron.