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.

Un ejemplo completo: alguien le escribe «Quiero info» a tu número, esperás diez minutos, revisás tu CRM y le respondés. Los tiempos y las condiciones son tuyos: la API te da las piezas y Funnelchat se ocupa de WhatsApp.

Lo que necesitás:

  • Una clave con webhooks:write (administrar la suscripción), conversations:read (leer contactos y la ventana, y recibir los eventos de mensajes), conversations:write (guardar datos del contacto) y conversations:send (responder). Ver Permisos.
  • Un servidor con una URL https pública para recibir los eventos.

1. Suscribite a los eventos

El flujo usa dos eventos: message.received (te escribieron) y message.status_changed (cambió el estado de un mensaje). Te suscribís con tu URL y los dos tipos, en la suscripción única de /api/v1/webhooks:

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/whatsapp",
    "events": ["message.received", "message.status_changed"],
    "description": "Mi flujo propio"
  }'
  • Responde 201 con la suscripción (id, status…) y el secret una sola vez: guardalo completo, es con lo que verificás la firma de cada entrega.
  • Pide webhooks:write y, por ser eventos de mensajes, también conversations:read.
  • message.status_changed se pide siempre por su nombre, como acá.
  • Suscripciones, firma y reintentos, en detalle: Cómo funcionan los webhooks.

2. Recibí el mensaje

Cada mensaje entrante llega a tu URL como un POST firmado con X-Funnelchat-Signature. Este es el ejemplo de message.received que publica la especificación:

{
  "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
  }
}

Verificá la firma sobre el cuerpo crudo, respondé 2xx enseguida y hacé el resto en segundo plano. Descartá los repetidos por id: un reintento trae el mismo. Los snippets de verificación, probados, están en Cómo funcionan los webhooks.

3. Tu lógica, tus tiempos

Esto es lo que antes te imponían nuestros flows: ahora es tuyo.

  • Guardá el estado de la conversación donde quieras: en tu base o en un campo personalizado del contacto en Funnelchat (Asignar un campo personalizado a contactos).
  • Programá la espera con tu propia cola o tu cron: diez minutos, un día o «cuando el CRM diga».
  • Decidí qué responder: consultá tu CRM, tu catálogo o tu IA.

4. Sabé desde qué número responder

Para responder necesitás el device_id del número que recibió el mensaje: el evento lo trae en data.device_id (y su teléfono en receiver.phone). Respondé por ese mismo número. Si querés su nombre o qué puede mandar, está en Listar números (permiso accounts:read).

5. Antes de responder, mirá la ventana

Con un número de WhatsApp Cloud (el oficial de Meta) solo podés mandar mensajes que no son plantilla si el contacto te escribió en las últimas 24 horas; si no, va una plantilla aprobada. Ver la ventana de 24 horas de un contacto te lo dice:

curl "https://api.funnelchat.com/api/v1/contacts/2001/meta-window?device_id=3" \
  -H "Authorization: Bearer <id>|fc_live_..." \
  -H "Accept: application/json"

Responde is_open (true o false) y meta_device_id. Si te olvidás de mirar, no se manda nada que rompa la regla: te frenamos con un error claro (paso 7).

6. Respondé

Ventana abierta: texto libre, con Enviar un mensaje a un contacto. device_id y contact_id van en el cuerpo: la especificación pide el contacto también ahí.

curl -X POST "https://api.funnelchat.com/api/v1/contacts/2001/message" \
  -H "Authorization: Bearer <id>|fc_live_..." \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": 3,
    "contact_id": 2001,
    "text": "¡Hola! Te paso la info…",
    "client_reference": "consulta-2001-1"
  }'

Responde 200 con message: el mensaje registrado, con su id y status: "queued". Guardá el id: es el message_id de los avisos del paso 8. client_reference es opcional y es tuya: vuelve en esos avisos, para relacionarlos con tu envío aunque la llamada no te haya respondido. Con file (la URL de un archivo) mandás una imagen, un video, un documento o un audio, y con options, botones o una lista: ver Mensajes.

Ventana cerrada: una plantilla aprobada, con Enviar una plantilla. En la ruta van el número (3) y el id de la plantilla (45), que sale de Listar plantillas.

curl -X POST "https://api.funnelchat.com/api/v1/devices/3/templates/45/send-template" \
  -H "Authorization: Bearer <id>|fc_live_..." \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "5491100000000",
    "contact_id": 2001,
    "template_variables": { "body": ["Lucía"] },
    "client_reference": "consulta-2001-1"
  }'

phone y contact_id son obligatorios; template_variables.body completa las variables del cuerpo de la plantilla, en orden. Responde 200 con el mensaje en cola, igual que el texto.

7. Si te frenamos

Si un envío rompe una regla de WhatsApp, no sale ni queda registrado: te devolvemos un error y no se lo mandamos a Meta.

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{
  "error": "window_closed",
  "message": "<explicación>"
}
RespuestaQué pasóQué hacer
422 window_closedUn mensaje que no es plantilla con la ventana cerrada, en un número de WhatsApp Cloud.Mandá una plantilla aprobada.
422 antiblock_duplicate_contentEl mismo contenido repetido al mismo contacto.No repitas el texto: cambialo o esperá.
422 antiblock_unanswered_limitDemasiados mensajes seguidos a alguien que no contesta.Esperá a que el contacto responda.
429 rate_limitedSuperaste el ritmo de tu número: 60 mensajes por segundo en un número de WhatsApp Cloud, 15 en coexistencia y sin límite por QR; además, 10 por minuto al mismo contacto en lo que sale por WhatsApp Cloud. Se cuenta por número, no por clave. Trae Retry-After.Esperá lo que indica Retry-After y reintentá.
422 recipient_unsupportedEl contacto es un grupo, una lista de difusión o un canal.Por la API solo se les escribe a personas.

Decidí por el código HTTP y por error; message es para personas. Los límites exactos están en Reglas de WhatsApp, y el ritmo de cada tipo de número, en Ritmo por número.

8. Enterate de qué pasó

Cada cambio de estado de un mensaje llega como message.status_changed:

{
  "id": "evt_…",
  "type": "message.status_changed",
  "version": 1,
  "occurred_at": "2026-10-15T14:02:11Z",
  "data": {
    "message_id": 1002,
    "contact_id": 2001,
    "device_id": 4521,
    "status": "read",
    "previous_status": "delivered",
    "occurred_at": "2026-10-15T14:02:10Z",
    "client_reference": "consulta-2001-1"
  }
}

sent → delivered → read, o failed con error (code, title y reason). Llega un aviso por cambio, nunca uno que retroceda. message_id es el id que te devolvió el envío. Si preferís preguntar en lugar de esperar el aviso, Ver un mensaje te da el estado de un mensaje por su id. Todo el detalle, en Estados de mensaje.

Con estas piezas armás cualquier flujo

Secuencias, recordatorios, un bot propio o tu IA: nada te obliga a usar nuestros flows. Si ya los usás, siguen ahí, en Funciones de Funnelchat.

Probalo acá

La ventana de 24 horas de un contacto, con tu clave. Es una lectura: no manda nada.

Ver la ventana de 24 horas de un contacto

GET
/contacts/{contact}/meta-window

Permiso requerido: conversations:read

Autorización

http
headerAuthorizationBearer <clave>

Parámetros de la ruta

contact*integer

Parámetros de consulta

device_id*integer

Cuerpo de la respuesta

OK

application/json
  1. response
is_open*boolean
meta_device_id*null
expires_at*null
curl -X GET "https://example.com/contacts/0/meta-window?device_id=0"
{  "is_open": true,  "meta_device_id": null,  "expires_at": null}

En esta página