Desarrollo de una aplicación de módulo de comunicaciones
Documentación para desarrolladores que implementan su propia aplicación que se conecta a Flowlu como un canal de comunicación (MiniApp) en el módulo de comunicaciones.
1. Descripción general
Qué es un canal MiniApp
Una MiniApp es un tipo de canal en el Contact Center de Flowlu que permite que tu aplicación (mensajería, widget de chat, portal corporativo, etc.) intercambie mensajes con los gerentes de Flowlu.
Para una MiniApp, tú mismo implementas ambos lados de la comunicación:
- Canal entrante (Inbound) — tu servidor envía solicitudes HTTP a Flowlu cuando un usuario escribe/edita/elimina un mensaje.
- Canal saliente (Outbound) — Flowlu envía solicitudes HTTP a tu servidor cuando un gerente responde o inicia un chat.
Qué se debe hacer
- Crear una aplicación externa en Flowlu y describir el manifiesto de la aplicación con la ubicación de integración "Communication service connection".
- Implementar una página de conexión — esta se abre en un iframe dentro de Flowlu cuando un gerente configura la integración.
- Implementar un receptor de webhooks salientes en tu servidor para manejar las solicitudes de Flowlu.
- Conectarse a la API de Flowlu para crear/actualizar el canal, enviar webhooks entrantes e informar errores.
Especificación de hooks (OpenAPI 3.1)
Este documento describe la integración en su conjunto. Los esquemas completos legibles por máquina para las cargas útiles (payloads) de los hooks se adjuntan al artículo como dos archivos:
- incoming_events.yaml — eventos entrantes (de tu aplicación a Flowlu).
- outgoing_hooks.yaml — webhooks salientes (de Flowlu a tu aplicación).
Puedes abrir los archivos YAML en un formato legible utilizando el servicio en línea ReDoc — https://redocly.github.io/redoc/ (carga el archivo descargado en el formulario de la página).
2. Arquitectura y principios de integración
Existen tres canales de interacción entre las partes:
| Canal | Iniciado por | Propósito |
| Página de conexión en iframe | Flowlu la abre en un iframe, realiza un POST | El usuario configura la integración en Flowlu; tu aplicación recibe el contexto del usuario (incluyendo auth.access_token) |
| API de Flowlu | Tu servidor | Crear/actualizar/eliminar un canal, enviar webhooks entrantes, informar errores |
| Webhooks salientes | Flowlu | Entrega de mensajes del gerente, iniciación de chats, eventos del ciclo de vida del canal |
Todos los mensajes de API/webhook transmiten JSON en el siguiente formato:
{ "method": "<method_name>", "payload": { /* fields */ } }
Glosario
| Término | Descripción |
| App Externa | Tu código — lado del servidor + página de conexión en iframe |
| Manifiesto de la aplicación | Configuración de tu aplicación en Flowlu: ubicaciones de integración, roles y permisos, URLs |
| Ubicación de integración (placement) | Un punto en la IU de Flowlu donde se abre tu página. Para el Contact Center, este es contactcenter.service.wizard ("Communication service connection") |
| app.id | El UUID de tu aplicación en Flowlu. Se envía en la solicitud POST al iframe |
| account.id | ID numérico de la cuenta de Flowlu donde se conecta el canal. Se envía en la solicitud POST al iframe |
| domain | El dominio de la cuenta de Flowlu, ej., mi.flowlu.com. Se envía en la solicitud POST al iframe; se usa como URL base para solicitudes de API |
| auth.access_token | Token OAuth2 del usuario que abrió el iframe. Úsalo para llamar a la API en nombre de este usuario |
| Canal de comunicación (bot) | Una integración conectada específica: tu bot_token. Creado a través de la API |
| bot_id (UUID del canal del lado del CRM) | El UUID del canal, devuelto por /contactcenter/bot/create. Se usa en las URLs de webhooks entrantes |
| bot_token | Identificador de integración en tu lado, ej., el ID del canal en tu sistema. Tú estableces esto durante la creación del canal y Flowlu lo devuelve en cada hook saliente como channel_id |
| event_id | Identificador del evento de webhook saliente. Necesario para enviar una respuesta de error si la solicitud no pudo procesarse; también se usa para deduplicar eventos del CRM |
| external_chat_id / external_message_id / external_user_id | Identificadores para el chat / mensaje / usuario en tu sistema |
| inner_message_id | Identificador del mensaje en Flowlu. Se envía en el hook saliente message.new.personal, debe devolverse en message.completed.personal |
3. Inicio rápido
La ruta completa desde cero hasta una integración funcional:
- Registra la aplicación en Flowlu (página /module/miniapps/cabinet), añade la ubicación de integración contactcenter.service.wizard con la URL de tu página de conexión. Otorga a la aplicación acceso al módulo de comunicaciones.
- Implementa la página de conexión en iframe. Flowlu realiza un POST a esta página con los datos de la cuenta y un token OAuth2 de usuario. Conecta el JS SDK de Flowlu para una operación conveniente del iframe.
- Implementa un receptor de webhooks salientes en tu servidor en una URL persistente (especificarás esta URL al crear el canal).
- Crea un canal de comunicación mediante POST /api/v1/module/contactcenter/bot/create en nombre del usuario que abrió el iframe.
- Intercambio de mensajes:
- Cuando un usuario escribe — envías message.new.personal a /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}.
- Cuando un gerente escribe — Flowlu realiza un POST a tu webhook_url; tú lo entregas y confirmas mediante message.completed.personal.
4. Creación de una aplicación en Flowlu
4.1 Registro en el gabinete
- Ve a https://{tu-dominio}/module/miniapps/cabinet.
- Haz clic en Create App, completa los campos principales y guarda.
- Abre la aplicación creada y ve a la sección de edición del manifiesto de la aplicación.
4.2 Añadir una ubicación de integración
Añade la ubicación de integración "Communication service connection" (contactcenter.service.wizard).
Campos de la ubicación de integración:
| Campo | Descripción |
| id | Identificador arbitrario de la ubicación de integración dentro de tu manifiesto de la aplicación |
| Header | El nombre que el gerente verá en la lista de posibles canales en el Contact Center |
| Icon (src) | Enlace HTTPS público al icono del canal |
| Action | Cómo se abrirá la página de conexión: ventana modal, panel lateral o nueva pestaña |
| Title | Título de la página abierta (si se selecciona ventana modal o panel lateral) |
| Size | Tamaño de la ventana modal / panel lateral |
| iframe (src) | URL de tu página de conexión. Flowlu la abrirá dentro de su interfaz |
4.3 Roles y permisos
En la configuración del manifiesto de la aplicación, otorga a la aplicación acceso a las APIs de los módulos a los que accederá:
- Communications — obligatorio; de lo contrario, no podrás crear/actualizar un canal a través de la API.
- Users/Employees — si tu aplicación almacena datos de usuario y los solicitará a través de /api/v1/module/core/user/get.
Sin el acceso otorgado, la API devolverá un error de autorización.
Después de guardar el manifiesto de la aplicación, en la página /_module/contactcenter/view/index?_ref=menu_app&tab=connected_bots, tu integración aparecerá en la lista de posibles canales al hacer clic en el botón "Connect". Al hacer clic, Flowlu abrirá tu iframe (src).
5. Ubicación de integración: página de conexión en iframe
Cuando un gerente hace clic en el elemento de tu integración en la lista de canales, Flowlu abre la URL que especificaste en iframe (src) en un iframe y le envía una solicitud POST con el contexto.
5.1 Qué se envía en el POST
{
"domain": "my.flowlu.com",
"language": "en",
"placement": {
"id": "123abc",
"code": "contactcenter.service.wizard"
},
"https": "1",
"account": {
"id": "123456"
},
"app": {
"id": "e9277716-2840-11f1-9ce9-4a62868fa1db",
"version": "1.0.0"
},
"auth": {
"expires_at": "2026-03-27 17:29:33",
"access_token": "...",
"refresh_token": "..."
},
"ratelimit": {
"limit": "16"
},
"bot_id": ""
}
| Campo | Descripción |
| domain | Dominio de la cuenta de Flowlu. Úsalo como URL base: https://{domain}/api/v1/... |
| language | Idioma de la interfaz de usuario |
| placement.id | ID del punto de integración de tu manifiesto |
| placement.code | Código del punto de integración. Para el Contact Center — siempre contactcenter.service.wizard |
| account.id | ID numérico de la cuenta de Flowlu. Se usa en las URLs de webhooks entrantes |
| app.id | UUID de tu aplicación |
| app.version | Versión actual del manifiesto (relevante para aplicaciones públicas) |
| auth.access_token | Token OAuth2 del usuario que abrió el iframe |
| auth.refresh_token | Token de actualización (refresh token) del usuario |
| auth.expires_at | Tiempo de expiración del access_token |
| ratelimit.limit | Límite de solicitudes de API |
| bot_id | UUID de un canal existente. Si está presente — es una página de edición. Si no — es una página de creación para un nuevo canal |
5.2 Frescura del token y datos de usuario
Los datos de auth.* siempre están actualizados en el momento en que se abre el iframe — Flowlu verifica y actualiza automáticamente los tokens antes de renderizar la página.
Si tu aplicación almacena datos de usuario en su lado, realiza una solicitud a /api/v1/module/core/user/get después de recibir el POST y actualiza tu copia:
GET https://{domain}/api/v1/module/core/user/get
Authorization: Bearer {auth.access_token}
Para que este endpoint funcione, el acceso correspondiente debe estar otorgado en el manifiesto.
5.3 Flowlu JS SDK
Para trabajar desde el iframe (llamar a diálogos nativos, notificaciones toast, cerrar el frame, leer el contexto), conecta el JS SDK oficial.
El SDK proporciona acceso conveniente al dominio, app.id, el usuario actual y las primitivas de la IU de Flowlu.
6. Autenticación
6.1 Solicitudes de tu aplicación a Flowlu
API (creación/actualización de canales, obtención de usuarios):
Encabezado Authorization: Bearer {auth.access_token}. El token es específico del usuario, emitido cuando se abre el iframe. La solicitud se ejecuta en nombre de este usuario.
Webhooks entrantes (/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}):
La autenticación es a través del bot_id secreto (UUID del canal del lado del CRM) en la URL. El bot_id es generado por el servidor, inmutable y es un secreto — almacénalo como una clave de API. account_id es el ID de tu portal de Flowlu, que llega en la solicitud POST al iframe.
6.2 Solicitudes de Flowlu a tu aplicación
Los webhooks salientes llegan a tu webhook_url sin firma y sin token Bearer. La propia URL sirve como prueba de autenticidad: si es impredecible, una filtración es poco probable.
Recomendaciones:
- Verifica adicionalmente que el channel_id que llega en la carga útil coincida con el bot_token de tu integración.
- Usa HTTPS en tu lado.
7. Creación de un canal de comunicación a través de la API
7.1 Endpoint
POST https://{domain}/api/v1/module/contactcenter/bot/create
Authorization: Bearer {auth.access_token}
Content-Type: application/json
Donde {domain} es el dominio de la solicitud POST al iframe.
7.2 Carga útil (Payload)
{
"name": "<channel name set by the manager>",
"webhook_url": "https:///webhook/flowlu",
"bot_token": "<your integration identifier>",
"active": 1,
"can_write_first": true
}
| Campo | Tipo | Requerido | Descripción |
| name | string | sí | Nombre del canal mostrado al gerente en el Contact Center. Generalmente establecido por el gerente en la página de conexión |
| webhook_url | string | sí | URL HTTPS de tu servidor para recibir webhooks salientes |
| bot_token | string | sí | Identificador de integración en tu lado. Devuelto en cada webhook saliente como channel_id — se usa para identificar a qué canal pertenece el evento |
| active | int / bool | no | 1 — activo (predeterminado), 0 — deshabilitado. Un canal desactivado no acepta hooks entrantes (responde con 409) |
| can_write_first | boolean | no | true — el botón "Escribir primero" está disponible para el gerente. Predeterminado es false |
7.3 Respuesta
{
"data": {
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Mi Canal",
"webhook_url": "https:///webhook/flowlu",
"bot_token": "...",
"active": true
}
}
- uuid — este es el bot_id que usas en las URLs de webhooks entrantes. Este es un secreto.
- id — ID numérico interno del canal para actualizaciones/eliminaciones posteriores.
Guarda el uuid y el id en la base de datos de tu aplicación, vinculándolos a tu integración interna.
7.4 Edición
Si llegó un bot_id en la solicitud POST al iframe — es una página de edición. Busca el canal por bot_id y actualiza sus campos a través del método de actualización de la API (name/webhook_url/active/can_write_first).
7.5 Eliminación de un canal
Un canal puede eliminarse a través de la API. Si el canal tenía un webhook_url establecido al ser eliminado, Flowlu te enviará un hook saliente bot.deleted — este es un buen momento para limpiar los datos relacionados en tu lado.
8. Webhooks entrantes (tu aplicación → Flowlu)
Los esquemas de carga útil completos para todos los métodos de esta sección están en el archivo incoming_events.yaml adjunto al artículo (OpenAPI 3.1).
8.1 Endpoint y estructura general
POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}
Content-Type: application/json
Donde:
- {domain} — dominio de la cuenta (lo guardaste durante la creación del canal);
- {account_id} — account.id de la solicitud POST al iframe;
- {bot_id} — uuid del canal devuelto por /contactcenter/bot/create.
Cuerpo: { "method": "<...>", "payload": { ... } }.
8.2 Códigos de respuesta
| HTTP | Cuerpo | Significado |
| 200 | {"success": true} | Evento aceptado |
| 400 | {"success": false, "message": ""} | Carga útil inválida |
| 404 | {"success": false, "message": "Not found"} | Mensaje no encontrado o pertenece a otro canal |
| 409 | {"success": false, "error": "...", "message": "..."} | Canal desactivado o cuenta bloqueada |
| 410 | vacío | Canal o cuenta eliminada |
| 500 | {"success": false, "message": "Internal error"} | Error temporal de Flowlu |
Nota: 409 y 410 sirven como una ruta alternativa para detectar desactivación/eliminación: si por alguna razón no recibiste bot.deactivated/bot.deleted, estos códigos actúan como salvaguarda.
8.3 message.new.personal — nuevo mensaje del canal externo
Cuándo llamar: un usuario en tu aplicación envía un mensaje al chat, o necesitas registrar un mensaje del gerente enviado fuera de Flowlu (eco)
{
"method": "message.new.personal",
"payload": {
"text": "Hola, sobre el pedido #42...",
"send_date": 1710752400,
"external_message_id": "msg_123",
"external_user_id": "user_456",
"external_chat_id": "chat_789",
"direction": 0,
"attachments": [],
"user_data": {
"name": "Juan Pérez",
"username": "juan",
"phone": "+1234567890",
"email": "[email protected]",
"avatar_url": "https://example.com/avatar.jpg",
"public_link": "https://example.com/juan"
}
}
}
| Campo | Tipo | Requerido | Descripción |
| text | string | no (por defecto "") | Texto del mensaje |
| send_date | int | no (por defecto time()) | Unix timestamp (segundos) |
| external_message_id | string | sí | ID del mensaje en tu sistema. Se usa para deduplicación |
| external_user_id | string | sí | ID del usuario interlocutor en tu sistema |
| external_chat_id | string | sí | ID del chat en tu sistema |
| direction | int | no | 0 — entrante (cliente → gerente), valor por defecto. 1 — saliente (eco de un mensaje del gerente enviado fuera de Flowlu) |
| attachments | array | no | Lista de adjuntos |
| user_data | object | no | Datos del interlocutor |
user_data contiene los datos del interlocutor, no del remitente. En mensajes salientes (direction = 1), este sigue siendo el cliente (el destinatario), no el gerente.
external_user_id es el ID del interlocutor, no del remitente. Para mensajes de eco salientes, este sigue siendo el ID del cliente, no el ID del gerente.
Idempotencia: los mensajes se deduplican por external_message_id dentro de un mismo hilo. Una solicitud repetida con el mismo ID no tiene efecto (200 OK, sin efectos secundarios).
8.4 message.edit.personal — editar un mensaje
{
"method": "message.edit.personal",
"payload": {
"external_message_id": "msg_123",
"edited_date": 1710752400,
"new_text": ""Texto actualizado"
}
}
| Campo | Tipo | Requerido | Descripción |
| external_message_id | string | sí | ID del mensaje que se está editando |
| edited_date | int | no (por defecto time()) | Unix timestamp de la edición |
| new_text | string | no (por defecto "") | Nuevo texto |
Si el mensaje no se encuentra o pertenece a otro canal — 404.
8.5 message.delete.personal — eliminar un mensaje
{
"method": "message.delete.personal",
"payload": {
"external_message_id": "msg_123",
"deleted_date": 1710752400
}
}
| Campo | Tipo | Requerido | Descripción |
| external_message_id | string | sí | ID del mensaje que se está eliminando |
| deleted_date | int | no | Unix timestamp de la eliminación |
La eliminación es suave (soft) — el mensaje se oculta para el gerente. Solo puedes eliminar mensajes de tu propio canal; intentar eliminar los de otro devolverá 404.
8.6 message.completed.personal — confirmación de entrega
Se envía después de que un mensaje del hook saliente message.new.personal se entrega realmente a tu canal y se le asigna un external_message_id.
Sin este evento, el mensaje del gerente permanece en Flowlu con el estado "enviando".
{
"method": "message.completed.personal",
"payload": {
"inner_message_id": 9001,
"external_message_id": "ext_msg_555"
}
}
| Campo | Tipo | Requerido | Descripción |
| inner_message_id | int | sí | ID del mensaje en Flowlu (enviado en message.new.personal saliente) |
| external_message_id | string | sí | ID del mensaje en tu sistema |
8.7 error — fallo al procesar hook saliente
Se envía si tu aplicación no pudo procesar un webhook saliente message.new.personal o chat.init.personal (ej., chat cerrado, el usuario bloqueó al bot, el destinatario no existe en el canal externo).
{
"method": "error",
"payload": {
"event_id": "<event_id value from the outbound hook>",
"message": "User not found"
}
}
| Campo | Tipo | Requerido | Descripción |
| event_id | string | sí | El valor de event_id del webhook saliente original — devuelto exactamente como se recibió |
| message | string | no | Texto libre para logs |
event_id es un token opaco. Flowlu lo usa para entender exactamente a qué evento se refiere el error y actualizar correctamente el estado del mensaje/chat. No lo analices ni lo modifiques.
Efecto:
- Si event_id corresponde a message.new.personal saliente — el mensaje del gerente cambia al estado "no entregado" (icono de error en la IU).
- Si corresponde a chat.init.personal — el gerente recibe una notificación sobre el fallo al iniciar el chat.
event_id tiene una vida limitada en el lado de Flowlu — envía el error tan pronto como identifiques el problema. No necesitas almacenar el event_id más allá del procesamiento de la solicitud actual.
9. Webhooks salientes (Flowlu → tu aplicación)
Los esquemas de carga útil completos para todos los hooks de esta sección están en el archivo outgoing_hooks.yaml adjunto al artículo (OpenAPI 3.1).
9.1 Principio: hook = trigger, la entrega es asíncrona
Un webhook saliente es una señal de disparo: "el gerente escribe, envíalo al canal externo", "el gerente inicia un chat, ábrelo en tu lado", etc. Flowlu no espera el resultado real de la entrega del mensaje en la respuesta HTTP.
El flujo de procesamiento se ve así:
- Flowlu realiza un POST a tu webhook_url.
- Tú respondes con 2xx tan pronto como aceptes la solicitud para procesamiento. La respuesta significa solo "aceptado".
- Luego, entregas el mensaje al usuario en tu lado (esto puede tomar cualquier cantidad de tiempo).
- Finalmente, envías el webhook entrante correspondiente a Flowlu:
- message.completed.personal — si la entrega es exitosa;
- error — si el procesamiento falló.
Sin el contra-hook, el mensaje del gerente en Flowlu permanece en estado "enviando" (para message.new.personal) o no se convertirá en un hilo real (para chat.init.personal).
9.2 Parámetros generales de la solicitud
- Endpoint: Tu webhook_url especificado durante la creación del canal.
- Método: POST, Content-Type: application/json.
- Cuerpo: { "method": "<...>", "payload": { ... } }.
- Respuesta: Cualquier 2xx (200/201/202) se trata como éxito — el evento es aceptado para procesamiento. El servidor no analiza el cuerpo de la respuesta.
9.3 Reintentos y tiempos de espera (Timeouts)
En caso de una respuesta que no sea 2xx o un error de red, Flowlu realiza hasta 3 intentos con un tiempo de espera exponencial (exponential backoff) de [500, 1000, 2000] ms:
| Intento | Cuándo |
| 1 | Inmediatamente |
| 2 | 0.5s después del fallo del Intento 1 |
| 3 | 1s después del fallo del Intento 2 |
Después del tercer intento fallido, el contador de fallos del canal se incrementa. Al llegar al décimo fallo consecutivo, el canal se desactiva automáticamente y recibirás un hook bot.deactivated con la razón: "auto_unreachable".
Clasificación de respuestas:
- 200/201/202 → éxito. El contador de fallos se reinicia.
- 400–499 → rechazo lógico (tu servidor respondió pero se negó a procesar). El canal no se desactiva, pero el mensaje del gerente correspondiente se marcará como "no entregado". No se realizan reintentos.
- 5xx, timeouts, errores DNS/TCP/TLS → fallo de infraestructura. Se realizarán hasta 3 intentos; el contador de fallos se incrementa.
- Las redirecciones (3xx) están prohibidas — responde con 200 directamente.
El tiempo de espera para una solicitud individual es de aproximadamente diez segundos. Haz que el procesamiento sea rápido; mueve el trabajo pesado a una tarea en segundo plano.
9.4 message.new.personal — el gerente envió un mensaje
{
"method": "message.new.personal",
"payload": {
"channel_id": "<your bot_token>",
"inner_message_id": "9001",
"external_chat_id": "",
"text": "¡Hola! ¿Cómo puedo ayudarte?",
"timestamp": 1710752400,
"event_id": "<opaque token>",
"attachments": []
}
}
| Campo | Tipo | Descripción |
| channel_id | string | Tu bot_token proporcionado durante la creación del canal. Úsalo para identificar la integración si tienes múltiples canales en una sola webhook_url |
| inner_message_id | string | ID del mensaje en Flowlu. Guárdalo — lo necesitarás para message.completed.personal |
| external_chat_id | string | ID del chat en tu sistema (tú lo enviaste en message.new.personal) |
| text | string | Texto del mensaje |
| timestamp | int | Unix timestamp del envío |
| event_id | string | Token de evento opaco. Solo es necesario si envías una respuesta de error |
| attachments | array | Lista de adjuntos |
Qué hacer:
- Responde con 200 inmediatamente.
- Busca el chat por external_chat_id y entrega el mensaje al usuario.
- Tras la entrega — envía message.completed.personal con inner_message_id y tu external_message_id.
- En caso de error — envía error con el event_id proporcionado.
9.5 chat.init.personal — el gerente inicia un chat
El gerente hizo clic en "Escribir primero". El chat en el lado de Flowlu aún no se ha creado — aparecerá cuando tu aplicación envíe message.new.personal con un nuevo external_chat_id.
Disponible solo si el canal tiene can_write_first = true.
{
"method": "chat.init.personal",
"payload": {
"channel_id": "<your bot_token>",
"to": {
"phone": "+1234567890",
"email": "[email protected]",
"name": "Juan Pérez",
"other": null
},
"message": { "text": "¡Hola!" },
"event_id": "<opaque token>"
}
}
| Campo | Tipo | Descripción |
| channel_id | string | Tu bot_token |
| to | object | Destinatario |
| message.text | string | Texto del primer mensaje |
| event_id | string | Token de evento opaco |
Qué hacer:
- Responde con 200 inmediatamente.
- Busca/crea al usuario en tu lado usando los campos de "to" (prioriza según te convenga: phone → email → other → name).
- Abre un chat con este usuario y obtén tu external_chat_id.
- Entrega message.text al usuario.
- Envía message.new.personal con este external_chat_id y direction: 1 (eco del gerente) — esto crea el chat en Flowlu.
- En caso de error — envía error con el event_id proporcionado.
9.6 bot.activated / bot.deactivated / bot.deleted
Eventos del ciclo de vida del canal. Son del tipo "dispara y olvida" — la acción ya se completó en el lado de Flowlu; tu respuesta solo se necesita para los logs. Devuelve 2xx.
{
"method": "bot.deactivated",
"payload": {
"channel_id": "<your bot_token>",
"reason": "manual"
}
}
{
"method": "bot.activated",
"payload": { "channel_id": "<your bot_token>" }
}
{
"method": "bot.deleted",
"payload": { "channel_id": "<your bot_token>" }
}
Posibles valores de "reason" para bot.deactivated:
| Valor | Descripción |
| manual | El administrador de la cuenta deshabilitó manualmente el canal |
| auto_unreachable | Flowlu desactivó automáticamente el canal debido a que la webhook_url fue consistentemente inaccesible (10 fallos consecutivos). Después de que tu servidor sea accesible de nuevo, el administrador debe habilitar el canal manualmente |
| subscription_expired | La suscripción de la cuenta de Flowlu expiró |
10. Objetos de datos
10.1 UserData
Utilizado en message.new.personal. Todos los campos son opcionales.
{
"name": "Juan Pérez",
"username": "juan",
"phone": "+1234567890",
"email": "[email protected]",
"avatar_url": "https://example.com/avatar.jpg",
"public_link": "https://example.com/juan"
}
| Campo | Descripción |
| name | Nombre completo. Se divide en nombre/apellido por el primer espacio |
| username | Apodo/login en tu sistema |
| phone | Número de teléfono |
| Dirección de correo electrónico | |
| avatar_url | URL del avatar (será descargado) |
| public_link | Enlace público al perfil del usuario en tu aplicación |
10.2 Attachment (entrante)
{
"id": "att_1",
"type": "photo",
"url": "https://files.example.com/att_1.jpg",
"filename": "captura.jpg",
"size": 245678,
"metadata": null
}
| Campo | Tipo | Requerido | Descripción |
| id | string | sí | ID único del adjunto en tu sistema |
| url | string | sí | URL HTTPS para descarga. Debe ser accesible para el servidor de Flowlu |
| filename | string | sí | Nombre del archivo con extensión |
| type | string | no (por defecto file) | Tipo: photo, file, video, audio, location |
| size | int | no | Tamaño en bytes. Usado para pre-verificación de límites |
| metadata | object | no | Objeto JSON arbitrario |
El archivo se descarga inmediatamente después de recibir el webhook. Si la URL es temporal, debe permanecer válida durante al menos unos segundos después de enviar el webhook.
10.3 Attachment (saliente)
La estructura es idéntica a la entrante: id, type, url, filename, size, metadata.
- id — ID del archivo en Flowlu (enviado como string).
- type se determina por MIME: image/* → photo, video/* → video, audio/* → audio, otros → file.
- url — Enlace de descarga de un solo uso. Descarga inmediatamente; no almacenes la URL por mucho tiempo.
10.4 To
Utilizado en chat.init.personal. Al menos un campo debe estar lleno.
{
"phone": "+1234567890",
"email": "[email protected]",
"name": "Juan Pérez",
"other": "custom_user_id"
}
| Campo | Descripción |
| phone | Número de teléfono del destinatario en formato internacional |
| Correo electrónico del destinatario | |
| name | Nombre del destinatario (cuando phone/email no están disponibles) |
| other | Identificador arbitrario en tu sistema |
11. Límites
| Parámetro | Valor |
| Máx. canales MiniApp por cuenta | 10 |
| Número de adjuntos en un mensaje | 10 |
| Tamaño máximo de archivo | 50 MB |
| Tamaño máximo de imagen | 50 MB |
| Intentos de entrega de webhooks salientes | 3 |
| Backoff entre intentos | 0.5s / 1s / 2s |
| Umbral de auto-desactivación | 10 fallos consecutivos |
| Esquema de webhook_url permitido | Solo HTTPS |
12. Manejo de errores e idempotencia
12.1 Idempotencia de tus solicitudes
- message.new.personal se deduplica por external_message_id dentro de un hilo. Es seguro reintentar — un duplicado devolverá 200 OK sin crear un segundo mensaje.
- message.completed.personal es idempotente: después de la primera confirmación, las solicitudes repetidas para el mismo inner_message_id devolverán 200.
- message.edit.personal y message.delete.personal no son estrictamente idempotentes (la edición repetida sobrescribirá el texto, la eliminación repetida devolverá 404). No reintentes sin motivo.
12.2 Idempotencia de los webhooks salientes
Flowlu no garantiza la entrega exactamente una vez: el mismo webhook puede llegar varias veces (ej., tu servidor respondió 200, pero la respuesta se perdió en la red — seguirá un reintento).
Construye tu procesamiento de forma idempotente. Cada hook saliente transmite un event_id único — no hay dos hooks con el mismo event_id, por lo que es útil como clave de deduplicación: recuerda los event_id procesados y simplemente devuelve 200 al repetirse.
Para message.new.personal, puedes usar adicionalmente inner_message_id: si el mensaje ya ha sido entregado al usuario, repite la respuesta 200 sin volver a entregarlo.
12.3 Canal desactivado — qué sucede
- Todos los hooks entrantes a /external/rest/contactcenter/bot/hook_miniapp/... devuelven 409 Conflict.
- No se envían hooks salientes a tu webhook_url.
Solo un administrador de la cuenta puede activar un canal (a través de la IU del Contact Center o tu página de conexión). Después de la activación, recibirás un hook bot.activated.
12.4 Errores 404 entre bots
Si intentas editar/eliminar/confirmar un mensaje que pertenece a otro canal en la misma cuenta, la API devolverá 404. Esta es una medida de seguridad para asegurar que una MiniApp no pueda afectar los mensajes de otra.
13. Escenario completo de extremo a extremo
13.1 Conexión del canal por el gerente
1. El gerente abre Contact Center → "Connect Channel" → selecciona tu integración
│
▼
2. Flowlu abre un iframe con tu página de conexión y envía un POST:
{ domain, account.id, app.id, auth.access_token, ... }
│
▼
3. Tu página de conexión:
- solicita el nombre del canal / otros ajustes al gerente
- POST https://{domain}/api/v1/module/contactcenter/bot/create
con Bearer {auth.access_token}
{ name, webhook_url, bot_token, active: 1 }
│
▼
4. Flowlu devuelve: { data: { id, uuid, ... } }
│
▼
5. Guardas el uuid (=bot_id) y el id. Cierras el iframe mediante JS SDK.
13.2 El usuario escribe, el gerente responde
1. El usuario escribe "Hola" en tu app │ ▼ 2. POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id} { "method": "message.new.personal", "payload": { "external_message_id": "msg_001", "external_chat_id": "chat_42", "external_user_id": "user_42", "text": "Hola", "send_date": 1710752400, "user_data": { "name": "Juan", "phone": "+1234567890" }, "attachments": [] } } ← 200 { "success": true } │ ▼ 3. El gerente responde "¡Hola! ¿En qué puedo ayudar?" │ ▼ 4. Flowlu → POST <your webhook_url> { "method": "message.new.personal", "payload": { "channel_id": "<your bot_token>", "inner_message_id": "9001", "external_chat_id": "chat_42", "text": "¡Hola! ¿En qué puedo ayudar?", "timestamp": 1710752700, "event_id": "<token>", "attachments": [] } } ← 200 (tú respondes inmediatamente) │ ▼ 5. Entregas el mensaje al usuario, recibes tu ID msg_xyz_789 │ ▼ 6. POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id} { "method": "message.completed.personal", "payload": { "inner_message_id": 9001, "external_message_id": "msg_xyz_789" } } ← 200 { "success": true } │ ▼ 7. El estado del mensaje del gerente cambia de "enviando" → "entregado".
13.3 El gerente escribe primero
1. El gerente hace clic en "Escribir primero" (disponible si can_write_first = true)
│
▼
2. Flowlu → POST <your webhook_url>
{
"method": "chat.init.personal",
"payload": {
"channel_id": "<your bot_token>",
"to": { "phone": "+79001234567" },
"message": { "text": "Vi su consulta..." },
"event_id": "<token>"
}
}
← 200
│
▼
3. Buscas al usuario por teléfono, abres el chat external_chat_id=chat_99,
entregas el mensaje
│
│ if failed — POST .../hook_miniapp/...
│ { "method": "error", "payload": { "event_id": "<token>", "message": "user not found" } }
│
▼
4. POST .../hook_miniapp/...
{
"method": "message.new.personal",
"payload": {
"external_message_id": "msg_init_1",
"external_chat_id": "chat_99",
"external_user_id": "user_42",
"text": "Vi su consulta...",
"send_date": 1710752800,
"user_data": { ... },
"direction": 1
}
}
← 200
│
▼
5. Se crea un hilo con external_chat_id=chat_99 en Flowlu. El gerente ve el chat.
14. Ejemplos de código
Marcadores de posición usados en los ejemplos:
- {domain} — mi.flowlu.com (valor del POST del iframe);
- {account_id} — 123456;
- {bot_id} — 550e8400-e29b-41d4-a716-446655440000 (UUID del canal);
- {access_token} — Token OAuth2 del usuario.
14.1 curl: crear un canal
curl -X POST 'https://{domain}/api/v1/module/contactcenter/bot/create' \
-H 'Authorization: Bearer {access_token}' \
-H 'Content-Type: application/json' \
-d '{
"name": "Mi Canal",
"webhook_url": "https://example.com/flowlu-webhook/8f7a3c",
"bot_token": "my-integration-id-42",
"active": 1,
"can_write_first": true
}'
14.2 curl: enviar un nuevo mensaje
curl -X POST 'https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}' \
-H 'Content-Type: application/json' \
-d '{
"method": "message.new.personal",
"payload": {
"text": "¡Hola!",
"send_date": 1710752400,
"external_message_id": "msg_001",
"external_user_id": "user_42",
"external_chat_id": "chat_42",
"attachments": [],
"user_data": { "name": "Juan Pérez", "phone": "+1234567890" }
}
}'
14.3 curl: confirmar entrega
curl -X POST 'https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}' \
-H 'Content-Type: application/json' \
-d '{
"method": "message.completed.personal",
"payload": {
"inner_message_id": 9001,
"external_message_id": "msg_xyz_789"
}
}'
14.4 Node.js: receptor de webhooks y cliente
import express from 'express';
import axios from 'axios';
const DOMAIN = 'my.flowlu.com';
const ACCOUNT_ID = 123456;
const BOT_ID = '550e8400-e29b-41d4-a716-446655440000';
const BOT_TOKEN = 'my-integration-id-42';
const hookUrl = `https://${DOMAIN}/external/rest/contactcenter/bot/hook_miniapp/${ACCOUNT_ID}/${BOT_ID}`;
const app = express();
app.use(express.json({ limit: '10mb' }));
// --- Outbound Webhook Receiver from Flowlu ---
app.post('/flowlu-webhook', async (req, res) => {
const { method, payload } = req.body;
if (payload?.channel_id && payload.channel_id !== BOT_TOKEN) {
return res.status(400).json({ error: 'unknown channel' });
}
// Respond with 200 immediately, run processing asynchronously.
res.status(200).json({ ok: true });
try {
switch (method) {
case 'message.new.personal': await deliverManagerMessage(payload); break;
case 'chat.init.personal': await openManagerInitiatedChat(payload); break;
case 'bot.activated':
case 'bot.deactivated':
case 'bot.deleted':
console.log(`Lifecycle: ${method}`, payload);
break;
default:
console.warn('Unknown method', method);
}
} catch (err) {
if (payload?.event_id) {
await reportError(payload.event_id, err.message).catch(() => {});
}
}
});
app.listen(3000);
// --- Deliver manager message to user + confirmation ---
async function deliverManagerMessage(payload) {
const { inner_message_id, external_chat_id, text, attachments } = payload;
const externalMessageId = await sendToUser(external_chat_id, text, attachments);
await axios.post(hookUrl, {
method: 'message.completed.personal',
payload: { inner_message_id, external_message_id: externalMessageId },
});
}
// --- Manager initiated chat ---
async function openManagerInitiatedChat(payload) {
const { to, message, event_id } = payload;
const user = await findUserByContact(to);
if (!user) throw new Error('user not found');
const chatId = await createOrOpenChat(user.id);
await sendToUser(chatId, message.text);
// Echo to Flowlu — without this the thread won't be created
await axios.post(hookUrl, {
method: 'message.new.personal',
payload: {
external_message_id: `init_${Date.now()}`,
external_chat_id: chatId,
external_user_id: user.id,
text: message.text,
send_date: Math.floor(Date.now() / 1000),
user_data: { name: user.name, phone: user.phone, email: user.email },
attachments: [],
direction: 1,
},
});
}
// --- Report delivery error ---
async function reportError(eventId, message) {
await axios.post(hookUrl, {
method: 'error',
payload: { event_id: eventId, message },
});
}
// --- When end-user writes to manager ---
async function userSentMessage({ userId, chatId, messageId, text, userData }) {
await axios.post(hookUrl, {
method: 'message.new.personal',
payload: {
text,
send_date: Math.floor(Date.now() / 1000),
external_message_id: messageId,
external_user_id: userId,
external_chat_id: chatId,
attachments: [],
user_data: userData,
},
});
}
// Stubs — implement for your stack
async function sendToUser(chatId, text, attachments) { /* ... */ return `local_${Date.now()}`; }
async function findUserByContact(to) { /* ... */ }
async function createOrOpenChat(userId) { /* ... */ }
14.5 PHP: cliente mínimo
post([
'method' => 'message.new.personal',
'payload' => [
'text' => $text,
'send_date' => time(),
'external_message_id' => $externalMessageId,
'external_user_id' => $externalUserId,
'external_chat_id' => $externalChatId,
'attachments' => $attachments,
'user_data' => $userData,
],
]);
}
public function confirmDelivery(int $innerMessageId, string $externalMessageId): void
{
$this->post([
'method' => 'message.completed.personal',
'payload' => [
'inner_message_id' => $innerMessageId,
'external_message_id' => $externalMessageId,
],
]);
}
public function reportError(string $eventId, string $message): void
{
$this->post([
'method' => 'error',
'payload' => ['event_id' => $eventId, 'message' => $message],
]);
}
private function post(array $body): void
{
$url = sprintf(
'https://%s/external/rest/contactcenter/bot/hook_miniapp/%d/%s',
$this->domain,
$this->accountId,
$this->botId
);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code < 200 || $code >= 300) {
throw new RuntimeException("Flowlu hook failed: HTTP $code, body: $response");
}
}
}
$client = new FlowluMiniAppClient('my.flowlu.com', 123456, '550e8400-e29b-41d4-a716-446655440000');
$client->sendUserMessage(
'msg_001',
'chat_42',
'user_42',
'Hola!',
['name' => 'Juan', 'phone' => '+1234567890']
);
Preguntas frecuentes (FAQ)
¿Puedo usar una sola webhook_url para múltiples canales?
Sí. Distínguelos por channel_id (= bot_token establecido durante la creación de cada canal).
¿Qué debo pasar en bot_token — algún secreto fijo?
Es el identificador de la integración en tu lado, no un secreto. Es conveniente usar tu propio UUID o el ID del canal de tu base de datos. Lo principal es que sea único dentro de tu aplicación e inmutable.
¿Qué debo pasar en user_data para un mensaje de eco del gerente (direction=1)?
Los datos del interlocutor (cliente), no los del gerente. El CRM usa esto para vincularlo al perfil del cliente.
¿Qué debo pasar en external_user_id para un mensaje saliente del gerente desde otro dispositivo?
El ID del interlocutor, no el del remitente. El mensaje se asocia con el chat donde ese interlocutor se está comunicando en cualquier caso.
El mensaje no aparece en Flowlu — ¿qué debo revisar?
- ¿Está activo el canal (active = 1)?
- ¿Es correcta la URL de la solicitud: /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}?
- ¿Es la respuesta 200 { "success": true }? 400 — carga útil inválida, 404 — canal/mensaje incorrecto, 409 — canal desactivado, 410 — canal eliminado.
- ¿Es external_message_id único dentro del hilo? Los duplicados se ignoran.
El gerente escribe, pero mi webhook no es llamado, ¿por qué?
- ¿Está configurada la webhook_url en el canal? Revísalo a través de una solicitud de API para leer el canal.
- ¿Responde la URL en un tiempo razonable (pocos segundos) y con un código 2xx?
- ¿Es la URL HTTPS y de acceso público?
- ¿Se ha alcanzado el umbral de auto-desactivación? Si el servidor respondió con errores 10 veces, el canal se deshabilita y espera una activación manual.
¿Qué debo hacer si recibo un webhook saliente duplicado?
Construye tu procesamiento de forma idempotente: para message.new.personal, usa event_id — si el evento ya fue procesado, devuelve 200 OK sin realizar más acciones.
¿Puedo cambiar la webhook_url de un canal existente?
Sí, a través de una solicitud de API para actualizar el canal con una nueva webhook_url.
¿Dónde puedo obtener el account_id del cliente?
En la solicitud POST al iframe, en el campo account.id. Este es un identificador de cuenta público usado en las URLs de webhooks entrantes.
¿Dónde puedo obtener el dominio del cliente?
En la solicitud POST al iframe, en el campo domain. Úsalo como URL base para todas las solicitudes de API a esa cuenta.
¿Se admiten confirmaciones de lectura ("mensaje leído")?
No. El método message.read.personal está reservado pero no se procesa en la versión actual.
- attach_file incoming_events.yaml (32.19KB)
- attach_file outgoing_hooks.yaml (16.44KB)