Desenvolvimento de um aplicativo de módulo de comunicações
Documentação para desenvolvedores que implementam seu próprio aplicativo para conectar ao Flowlu como um canal de comunicação (MiniApp) no módulo de comunicações.
1. Visão Geral
O que é um canal MiniApp
Um MiniApp é um tipo de canal no Contact Center do Flowlu que permite que seu aplicativo (mensageiro, widget de chat, portal corporativo, etc.) troque mensagens com os gerentes do Flowlu.
Para um MiniApp, você mesmo implementa os dois lados da comunicação:
- Canal de Entrada — seu servidor envia solicitações HTTP para o Flowlu quando um usuário escreve/edita/exclui uma mensagem.
- Canal de Saída — o Flowlu envia solicitações HTTP para o seu servidor quando um gerente responde ou inicia um chat.
O que precisa ser feito
- Criar um aplicativo externo no Flowlu e descrever o manifesto do aplicativo com o ponto de integração "Conexão de serviço de comunicação".
- Implementar uma página de conexão — ela abre em um iframe dentro do Flowlu quando um gerente configura a integração.
- Implementar um receptor de webhook de saída em seu servidor para lidar com as solicitações do Flowlu.
- Conectar-se à API do Flowlu para criar/atualizar o canal, enviar webhooks de entrada e relatar erros.
Especificação de hook (OpenAPI 3.1)
Este documento descreve a integração como um todo. Os esquemas completos legíveis por máquina para os payloads de hook estão anexados ao artigo como dois arquivos:
- incoming_events.yaml — eventos de entrada (do seu aplicativo para o Flowlu).
- outgoing_hooks.yaml — hooks de saída (do Flowlu para o seu aplicativo).
Você pode abrir os arquivos YAML em um formato legível usando o serviço online ReDoc — https://redocly.github.io/redoc/ (carregue o arquivo baixado no formulário da página).
2. Arquitetura e princípios de integração
Existem três canais de interação entre as partes:
| Canal | Iniciado Por | Propósito |
| Página de conexão em iframe | Flowlu abre em um iframe, realiza um POST | O usuário configura a integração no Flowlu; seu aplicativo recebe o contexto do usuário (incluindo auth.access_token) |
| API do Flowlu | Seu servidor | Criar/atualizar/excluir um canal, enviar webhooks de entrada, relatar erros |
| Webhooks de Saída | Flowlu | Entregar mensagens do gerente, iniciar chats, eventos de ciclo de vida do canal |
Todas as mensagens de API/webhook transmitem JSON no seguinte formato:
{ "method": "<method_name>", "payload": { /* fields */ } }
Glossário
| Termo | Descrição |
| Aplicativo Externo | Seu código — lado do servidor + página de conexão em iframe |
| Manifesto do Aplicativo | Configuração do seu aplicativo no Flowlu: pontos de integração, direitos de acesso, URLs |
| Ponto de integração (placement) | Um ponto na interface do Flowlu onde sua página abre. Para o Contact Center, é o contactcenter.service.wizard ("Conexão de serviço de comunicação") |
| app.id | O UUID do seu aplicativo no Flowlu. Enviado na solicitação POST para o iframe |
| account.id | ID numérico da conta do Flowlu onde o canal está sendo conectado. Enviado no POST para o iframe |
| domain | O domínio da conta Flowlu, ex: minha.flowlu.com. Enviado no POST para o iframe; usado como URL base para solicitações de API |
| auth.access_token | Token OAuth2 do usuário que abriu o iframe. Use-o para chamar a API em nome deste usuário |
| Canal de Comunicação (bot) | Uma integração conectada específica: seu bot_token. Criado via API |
| bot_id (UUID do canal no CRM) | O UUID do canal, retornado de /contactcenter/bot/create. Usado nas URLs de webhook de entrada |
| bot_token | Identificador da integração no seu lado, ex: o ID do canal no seu sistema. Você define isso durante a criação do canal, e o Flowlu o retorna em cada hook de saída como channel_id |
| event_id | Identificador do evento do webhook de saída. Necessário para enviar uma resposta de erro se a solicitação não puder ser processada; também pode ser usado para deduplicar eventos do CRM |
| external_chat_id / external_message_id / external_user_id | Identificadores para o chat / mensagem / usuário em seu sistema |
| inner_message_id | Identificador da mensagem no Flowlu. Enviado em outbound message.new.personal, deve ser retornado em message.completed.personal |
3. Início rápido
O caminho completo do zero até uma integração funcional:
- Registre o aplicativo no Flowlu (página /module/miniapps/cabinet), adicione o ponto de integração contactcenter.service.wizard com a URL da sua página de conexão. Conceda ao aplicativo acesso ao "Módulo de comunicações".
- Implemente a página de conexão em iframe. O Flowlu realiza um POST para esta página com dados da conta e um token OAuth2 do usuário. Conecte o Flowlu JS SDK para operação conveniente do iframe.
- Implemente um receptor de webhook de saída em seu servidor em uma URL persistente (você especificará esta URL ao criar o canal).
- Crie um canal de comunicação via POST /api/v1/module/contactcenter/bot/create em nome do usuário que abriu o iframe.
- Troque mensagens:
- Quando um usuário digita — você envia message.new.personal para /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}.
- Quando um gerente digita — o Flowlu realiza um POST para sua webhook_url; você a entrega e confirma via message.completed.personal.
4. Criando um aplicativo no Flowlu
4.1 Registro no gabinete
- Vá para https://{your-domain}/module/miniapps/cabinet.
- Clique em Criar aplicativo, preencha os campos principais e salve.
- Abra o aplicativo criado e vá para a seção de edição do manifesto do aplicativo.
4.2 Adicionando um ponto de integração
Adicione o ponto de integração "Conexão de serviço de comunicação" (contactcenter.service.wizard).
Campos do ponto de integração:
| Campo | Descrição |
| id | Identificador arbitrário do ponto de integração dentro do seu manifesto |
| Header | O nome que o gerente verá na lista de canais possíveis no Contact Center |
| Icon (src) | Link HTTPS público para o ícone do canal |
| Action | Como a página de conexão abrirá: janela modal, painel lateral ou nova aba |
| Title | Título da página aberta (se janela modal ou painel lateral for selecionado) |
| Size | Tamanho da janela modal / painel lateral |
| iframe (src) | URL da sua página de conexão. O Flowlu a abrirá dentro de sua interface |
4.3 Direitos de acesso
Nas configurações do manifesto, conceda ao aplicativo acesso às APIs dos módulos que ele acessará:
- Comunicações — obrigatório; caso contrário, você não poderá criar/atualizar um canal via API.
- Usuários/Funcionários — se o seu aplicativo armazenar dados de usuário e for solicitá-los via /api/v1/module/core/user/get.
Sem o acesso concedido, a API retornará um erro de autorização.
Após salvar o manifesto, na página /_module/contactcenter/view/index?_ref=menu_app&tab=connected_bots, sua integração aparecerá na lista de canais possíveis ao clicar no botão "Conectar". Ao clicar, o Flowlu abrirá seu iframe (src).
5. Ponto de integração: página de conexão em iframe
Quando um gerente clica no item de integração na lista de canais, o Flowlu abre a URL especificada em iframe (src) em um iframe e envia uma solicitação POST com o contexto para ela.
5.1 O que é enviado no POST
{
"domain": "my.flowlu.com",
"language": "pt",
"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 | Descrição |
| domain | Domínio da conta Flowlu. Use como URL base: https://{domain}/api/v1/... |
| language | Idioma da interface do usuário |
| placement.id | ID do ponto de integração do seu manifesto |
| placement.code | Código do ponto de integração. Para o Contact Center — sempre contactcenter.service.wizard |
| account.id | ID numérico da conta Flowlu. Usado nas URLs de webhook de entrada |
| app.id | UUID do seu aplicativo |
| app.version | Versão atual do manifesto (relevante para apps públicos) |
| auth.access_token | Token OAuth2 do usuário que abriu o iframe |
| auth.refresh_token | Token de atualização do usuário |
| auth.expires_at | Tempo de expiração do access_token |
| ratelimit.limit | Limite de solicitações de API |
| bot_id | UUID de um canal existente. Se presente — é uma página de edição. Se não — é uma página de criação para um novo canal |
5.2 Atualização de tokens e dados do usuário
Os dados auth.* estão sempre atualizados no momento em que o iframe é aberto — o Flowlu verifica e atualiza automaticamente os tokens antes de renderizar a página.
Se o seu aplicativo armazena dados de usuário do seu lado, realize uma solicitação para /api/v1/module/core/user/get após receber o POST e atualize sua cópia:
GET https://{domain}/api/v1/module/core/user/get Authorization: Bearer {auth.access_token}
Para que este endpoint funcione, o acesso correspondente deve ser concedido no manifesto.
5.3 Flowlu JS SDK
Para trabalhar a partir do iframe (chamando diálogos nativos, toasts, fechando o frame, lendo o contexto), conecte o oficial JS SDK.
O SDK fornece acesso conveniente ao domínio, app.id, ao usuário atual e às primitivas de interface do Flowlu.
6. Autenticação
6.1 Solicitações do seu app para o Flowlu
API (criação/atualização de canal, recuperação de usuário):
Cabeçalho Authorization: Bearer {auth.access_token}. O token é específico do usuário, emitido quando o iframe abre. A solicitação é executada em nome deste usuário.
Webhooks de Entrada (/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}):
A autenticação é via bot_id secreto (UUID do canal no CRM) na URL. O bot_id é gerado pelo servidor, imutável e é um segredo — armazene-o como uma chave de API. account_id é o ID do seu portal Flowlu, que vem na solicitação POST para o iframe.
6.2 Solicitações do Flowlu para o seu app
Webhooks de saída chegam à sua webhook_url sem uma assinatura e sem um token Bearer. A própria URL serve como prova de autenticidade: se for imprevisível, um vazamento é improvável.
Recomendações:
- Verifique adicionalmente se o channel_id que chega no payload corresponde ao seu bot_token de integração.
- Use HTTPS no seu lado.
7. Criando um canal de comunicação via API
7.1 Endpoint
POST https://{domain}/api/v1/module/contactcenter/bot/create Authorization: Bearer {auth.access_token} Content-Type: application/json
Onde {domain} é o domínio da solicitação POST para o iframe.
7.2 Payload
{
"name": "<nome do canal definido pelo gerente>",
"webhook_url": "https:///webhook/flowlu",
"bot_token": "<identificador da sua integração>",
"active": 1,
"can_write_first": true
}
| Campo | Tipo | Obrigatório | Descrição |
| name | string | sim | Nome do canal exibido para o gerente no Contact Center. Geralmente definido pelo gerente na página de conexão |
| webhook_url | string | sim | URL HTTPS do seu servidor para receber webhooks de saída |
| bot_token | string | sim | Identificador da integração no seu lado. Retornado em cada webhook de saída como channel_id — usado para que você possa identificar a qual canal o evento pertence |
| active | int / bool | não | 1 — ativo (padrão), 0 — desativado. Um canal desativado não aceita hooks de entrada (responde com 409) |
| can_write_first | boolean | não | true — o botão "Escrever primeiro" fica disponível para o gerente. O padrão é false |
7.3 Resposta
{
"data": {
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Meu Canal",
"webhook_url": "https:///webhook/flowlu",
"bot_token": "...",
"active": true
}
}
- uuid — este é o bot_id que você usa nas URLs de webhook de entrada. Isso é um segredo.
- id — ID numérico interno do canal para atualizações/exclusão subsequentes.
Salve o uuid e o id no banco de dados do seu aplicativo, vinculando-os à sua integração interna.
7.4 Edição
Se um bot_id chegou na solicitação POST para o iframe — é uma página de edição. Encontre o canal pelo bot_id e atualize seus campos via método de atualização da API (name/webhook_url/active/can_write_first).
7.5 Deletando um canal
Um canal pode ser excluído via API. Se o canal tinha uma webhook_url definida quando excluído, o Flowlu enviará a você um hook de saída bot.deleted — este é um bom momento para limpar os dados relacionados no seu lado.
8. Webhooks de entrada (seu app → Flowlu)
Esquemas de payload completos para todos os métodos nesta seção estão no arquivo incoming_events.yaml anexado ao artigo (OpenAPI 3.1).
8.1 Endpoint e estrutura geral
POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id} Content-Type: application/json
Onde:
- {domain} — domínio da conta (você o salvou durante a criação do canal);
- {account_id} — account.id da solicitação POST para o iframe;
- {bot_id} — uuid do canal retornado por /contactcenter/bot/create.
Corpo: { "method": "<...>", "payload": { ... } }.
8.2 Códigos de resposta
| HTTP | Corpo | Significado |
| 200 | {"success": true} | Evento aceito |
| 400 | {"success": false, "message": ""} | Payload inválido |
| 404 | {"success": false, "message": "Not found"} | Mensagem não encontrada ou pertence a outro canal |
| 409 | {"success": false, "error": "...", "message": "..."} | Canal desativado ou conta bloqueada |
| 410 | vazio | Canal ou conta excluída |
| 500 | {"success": false, "message": "Internal error"} | Erro temporário do Flowlu |
Nota: 409 e 410 servem como um caminho alternativo para detectar desativação/exclusão: se por algum motivo você não recebeu bot.deactivated/bot.deleted, esses códigos agem como uma salvaguarda.
8.3 message.new.personal — nova mensagem do canal externo
Quando chamar: um usuário no seu aplicativo envia uma mensagem para o chat, ou você precisa registrar uma mensagem do gerente enviada fora do Flowlu (eco).
{
"method": "message.new.personal",
"payload": {
"text": "Olá, sobre o 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": "John Doe",
"username": "john",
"phone": "+1234567890",
"email": "[email protected]",
"avatar_url": "https://example.com/avatar.jpg",
"public_link": "https://example.com/john"
}
}
}
| Campo | Tipo | Obrigatório | Descrição |
| text | string | não (padrão "") | Texto da mensagem |
| send_date | int | não (padrão time()) | Unix timestamp (segundos) |
| external_message_id | string | sim | ID da mensagem no seu sistema. Usado para deduplicação |
| external_user_id | string | sim | ID do usuário interlocutor no seu sistema |
| external_chat_id | string | sim | ID do chat no seu sistema |
| direction | int | não | 0 — entrada (cliente → gerente), valor padrão. 1 — saída (eco de uma mensagem do gerente enviada fora do Flowlu) |
| attachments | array | não | Lista de anexos |
| user_data | object | não | Dados do interlocutor |
user_data contém os dados do interlocutor, não do remetente. Em mensagens de saída (direction = 1), este ainda é o cliente (o destinatário), não o gerente.
external_user_id é o ID do interlocutor, não do remetente. Para mensagens de eco de saída, este ainda é o ID do cliente, não o ID do gerente.
Idempotência: as mensagens são deduplicadas por external_message_id dentro de uma única conversa. Uma solicitação repetida com o mesmo ID é uma operação nula (200 OK, sem efeitos colaterais).
8.4 message.edit.personal — editando uma mensagem
{
"method": "message.edit.personal",
"payload": {
"external_message_id": "msg_123",
"edited_date": 1710752400,
"new_text": "Texto atualizado"
}
}
| Campo | Tipo | Obrigatório | Descrição |
| external_message_id | string | sim | ID da mensagem sendo editada |
| edited_date | int | não (padrão time()) | Unix timestamp da edição |
| new_text | string | não (padrão "") | Novo texto |
Se a mensagem não for encontrada ou pertencer a outro canal — 404.
8.5 message.delete.personal — deletando uma mensagem
{
"method": "message.delete.personal",
"payload": {
"external_message_id": "msg_123",
"deleted_date": 1710752400
}
}
| Campo | Tipo | Obrigatório | Descrição |
| external_message_id | string | sim | ID da mensagem sendo excluída |
| deleted_date | int | não | Unix timestamp da exclusão |
A exclusão é suave — a mensagem é ocultada para o gerente. Você só pode excluir mensagens do seu próprio canal; tentar excluir a de outra pessoa retornará 404.
8.6 message.completed.personal — confirmação de entrega
Enviado após uma mensagem do hook de saída message.new.personal ser realmente entregue ao seu canal e receber um external_message_id.
Sem este evento, a mensagem do gerente permanece no Flowlu com o status "enviando".
{
"method": "message.completed.personal",
"payload": {
"inner_message_id": 9001,
"external_message_id": "ext_msg_555"
}
}
| Campo | Tipo | Obrigatório | Descrição |
| inner_message_id | int | sim | ID da mensagem no Flowlu (enviado no message.new.personal de saída) |
| external_message_id | string | sim | ID da mensagem no seu sistema |
8.7 error — falha ao processar hook de saída
Enviado se o seu aplicativo não pôde processar um webhook de saída message.new.personal ou chat.init.personal (ex: chat fechado, usuário bloqueou o bot, destinatário não existe no canal externo).
{
"method": "error",
"payload": {
"event_id": "<event_id value from the outbound hook>",
"message": "User not found"
}
}
| Campo | Tipo | Obrigatório | Descrição |
| event_id | string | sim | O valor event_id do webhook de saída original — retornado exatamente como recebido |
| message | string | não | Texto livre para logs |
event_id é um token opaco. O Flowlu o usa para entender exatamente a qual evento o erro se refere e atualizar corretamente o status da mensagem/chat. Não o processe nem o modifique.
Efeito:
- Se event_id corresponde ao message.new.personal de saída — a mensagem do gerente muda para o status "não entregue" (ícone de erro na interface).
- Se corresponde ao chat.init.personal — o gerente recebe uma notificação sobre uma falha na iniciação do chat.
event_id tem uma vida útil limitada no lado do Flowlu — envie o erro assim que identificar o problema. Você não precisa armazenar o event_id além do processamento da solicitação atual.
8.8 message.read.personal (reservado)
O método é aceito pelo servidor, mas não é processado na versão atual — é uma operação nula. Não construa lógica de negócios baseada em recibos de leitura: o status de leitura não é salvo em nenhum lugar e não é exibido para o gerente.
9. Webhooks de saída (Flowlu → seu app)
Esquemas de payload completos para todos os hooks nesta seção estão no arquivo outgoing_hooks.yaml anexado ao artigo (OpenAPI 3.1).
9.1 Princípio: hook = gatilho, entrega assíncrona
Um webhook de saída é um sinal de gatilho: "o gerente digita, envie para o canal externo", "o gerente inicia um chat, abra-o no seu lado", etc. O Flowlu não espera pelo resultado real da entrega da mensagem na resposta HTTP.
O fluxo de processamento funciona assim:
- O Flowlu realiza um POST para sua webhook_url.
- Você responde com 2xx assim que aceitar a solicitação para processamento. A resposta significa apenas "aceito".
- Em seguida, você entrega a mensagem ao usuário no seu lado (isso pode levar qualquer tempo).
- Finalmente, você envia um webhook de entrada correspondente ao Flowlu:
- message.completed.personal — se a entrega for bem-sucedida;
- error — se o processamento falhar.
Sem o contra-hook, a mensagem do gerente no Flowlu permanece no status "enviando" (para message.new.personal) ou não se tornará uma conversa real (para chat.init.personal).
9.2 Parâmetros gerais de solicitação
- Endpoint: Sua webhook_url especificada durante a criação do canal.
- Método: POST, Content-Type: application/json.
- Corpo: { "method": "<...>", "payload": { ... } }.
- Resposta: Qualquer 2xx (200/201/202) é tratado como sucesso — o evento é aceito para processamento. O servidor não processa o corpo da resposta.
9.3 Retentativas e timeouts
Em caso de uma resposta não 2xx ou um erro de rede, o Flowlu realiza até 3 tentativas com backoff exponencial [500, 1000, 2000] ms:
| Tentativa | Quando |
| 1 | Imediatamente |
| 2 | 0,5s após a falha da Tentativa 1 |
| 3 | 1s após a falha da Tentativa 2 |
Após a 3ª tentativa fracassada, o contador de falhas do canal é incrementado. Na 10ª falha consecutiva, o canal é desativado automaticamente e você receberá um hook bot.deactivated com o motivo: "auto_unreachable".
Classificação de resposta:
- 200/201/202 → sucesso. Contador de falhas redefinido.
- 400–499 → recusa lógica (seu servidor respondeu, mas se recusou a processar). O canal não é desativado, mas a mensagem do gerente correspondente será marcada como "não entregue". Nenhuma retentativa é realizada.
- 5xx, timeouts, erros de DNS/TCP/TLS → falha de infraestrutura. Até 3 tentativas com backoff serão realizadas; o contador de falhas é incrementado.
- Redirecionamentos (3xx) são proibidos — responda com 200 diretamente.
O tempo limite para uma solicitação individual é de aproximadamente dezenas de segundos. Torne o processamento rápido; mova o trabalho pesado para uma tarefa em segundo plano.
9.4 message.new.personal — gerente enviou uma mensagem
{
"method": "message.new.personal",
"payload": {
"channel_id": "<your bot_token>",
"inner_message_id": "9001",
"external_chat_id": "",
"text": "Olá! Como posso ajudar você?",
"timestamp": 1710752400,
"event_id": "<opaque token>",
"attachments": []
}
}
| Campo | Tipo | Descrição |
| channel_id | string | Seu bot_token fornecido durante a criação do canal. Use-o para identificar a integração |
| inner_message_id | string | ID da mensagem no Flowlu. Salve-o — você precisará dele para o message.completed.personal |
| external_chat_id | string | ID do chat no seu sistema (você o enviou no message.new.personal) |
| text | string | Texto da mensagem |
| timestamp | int | Unix timestamp do envio |
| event_id | string | Token de evento opaco. Necessário apenas se você enviar uma resposta de erro |
| attachments | array | Lista de anexos |
O que fazer:
- Responda com 200 imediatamente.
- Encontre o chat por external_chat_id e entregue a mensagem ao usuário.
- Após a entrega — envie message.completed.personal com inner_message_id e seu external_message_id.
- Em caso de erro — envie error com o event_id fornecido.
9.5 chat.init.personal — gerente inicia um chat
O gerente clicou em "Escrever primeiro". O chat no lado do Flowlu ainda não foi criado — ele aparecerá quando o seu aplicativo enviar message.new.personal com um novo external_chat_id.
Disponível apenas se o canal tiver can_write_first = true.
{
"method": "chat.init.personal",
"payload": {
"channel_id": "<your bot_token>",
"to": {
"phone": "+1234567890",
"email": "[email protected]",
"name": "John Doe",
"other": null
},
"message": { "text": "Olá!" },
"event_id": "<opaque token>"
}
}
| Campo | Tipo | Descrição |
| channel_id | string | Seu bot_token |
| to | object | Destinatário |
| message.text | string | Texto da primeira mensagem |
| event_id | string | Token de evento opaco |
O que fazer:
- Responda com 200 imediatamente.
- Encontre/crie o usuário no seu lado usando os campos to (priorize conforme sua necessidade: telefone → e-mail → outro → nome).
- Abra um chat com este usuário e obtenha seu external_chat_id.
- Entregue message.text ao usuário.
- Envie message.new.personal com este external_chat_id e direction: 1 (eco do gerente) — isso cria o chat no Flowlu.
- Em caso de erro — envie error com o event_id fornecido.
9.6 bot.activated / bot.deactivated / bot.deleted
Eventos de ciclo de vida do canal. Ação disparada e esquecida — a ação já está concluída no lado do Flowlu; sua resposta é necessária apenas para logs. Retorne 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>" }
}
Valores de motivo possíveis para bot.deactivated:
| Valor | Descrição |
| manual | Administrador da conta desativou manualmente o canal |
| auto_unreachable | Flowlu desativou automaticamente o canal devido a uma webhook_url consistentemente inacessível (10 falhas consecutivas) |
| subscription_expired | A assinatura da conta Flowlu expirou |
10. Objetos de dados
10.1 UserData
Usado no message.new.personal. Todos os campos são opcionais.
{
"name": "John Doe",
"username": "john",
"phone": "+1234567890",
"email": "[email protected]",
"avatar_url": "https://example.com/avatar.jpg",
"public_link": "https://example.com/john"
}
| Campo | Descrição |
| name | Nome completo. Dividido em nome/sobrenome pelo primeiro espaço |
| username | Apelido/login no seu sistema |
| phone | Número de telefone |
| Endereço de e-mail | |
| avatar_url | URL do avatar (será baixado) |
| public_link | Link público para o perfil do usuário no seu aplicativo |
10.2 Anexo (entrada)
{
"id": "att_1",
"type": "photo",
"url": "https://files.example.com/att_1.jpg",
"filename": "screenshot.jpg",
"size": 245678,
"metadata": null
}
| Campo | Tipo | Obrigatório | Descrição |
| id | string | sim | ID exclusivo do anexo no seu sistema |
| url | string | sim | URL HTTPS para download. Deve estar acessível para o servidor do Flowlu |
| filename | string | sim | Nome do arquivo com extensão |
| type | string | não (padrão file) | Tipo: photo, file, video, audio, location |
| size | int | não | Tamanho em bytes |
| metadata | object | não | Objeto JSON arbitrário |
10.3 Anexo (saída)
A estrutura é idêntica à de entrada: id, type, url, filename, size, metadata.
- id — ID do arquivo no Flowlu (enviado como uma string).
- o tipo é determinado pelo MIME: image/* → foto, video/* → vídeo, audio/* → áudio, outros → arquivo.
- url — Link de download de uso único. Baixe imediatamente; não armazene a URL por muito tempo.
10.4 To
Usado no chat.init.personal. Pelo menos um campo deve ser não vazio.
{
"phone": "+1234567890",
"email": "[email protected]",
"name": "John Doe",
"other": "custom_user_id"
}
| Campo | Descrição |
| phone | Número de telefone do destinatário em formato internacional |
| Endereço de e-mail do destinatário | |
| name | Nome do destinatário (quando telefone/e-mail não estiverem disponíveis) |
| other | Identificador arbitrário em seu sistema |
11. Limites
| Parâmetro | Valor |
| Canais MiniApp por conta | 10 |
| Número de anexos em uma mensagem | 10 |
| Tamanho máximo do arquivo | 50 MB |
| Tentativas de entrega de webhook de saída | 3 |
| Limite para desativação automática | 10 falhas consecutivas |
| Esquema de webhook_url permitido | Apenas HTTPS |
12. Tratamento de erros e idempotência
12.1 Idempotência das suas solicitações
- message.new.personal é deduplicado por external_message_id dentro de uma conversa. Seguro para tentar novamente.
- message.completed.personal é idempotente: solicitações repetidas para o mesmo inner_message_id retornarão 200.
- message.edit.personal e message.delete.personal não são estritamente idempotentes.
12.2 Idempotência de webhooks de saída
O Flowlu não garante a entrega "exatamente uma vez": o mesmo webhook pode chegar várias vezes (por exemplo, seu servidor respondeu 200, mas a resposta foi perdida na rede — uma nova tentativa será realizada).
Construa seu processamento de forma idempotente. Cada hook de saída transmite um event_id exclusivo — não existem dois hooks com o mesmo event_id, por isso ele é útil como uma chave de deduplicação: armazene os valores de event_id processados e simplesmente retorne 200 em caso de repetição.
Para message.new.personal, você pode usar adicionalmente o inner_message_id: se a mensagem já tiver sido entregue ao usuário, repita a resposta 200 sem realizar o reenvio.
12.3 Canal desativado — o que acontece
- Todos os hooks de entrada para /external/rest/contactcenter/bot/hook_miniapp/... retornam 409 Conflict.
- Hooks de saída não são enviados para sua webhook_url.
Apenas um administrador da conta pode ativar um canal (via interface do Contact Center ou sua página de conexão).
13. Cenário completo ponta a ponta
13.1 Conexão de canal pelo gerente
1. Gerente abre Contact Center → "Conectar Canal" → seleciona sua integração
│
▼
2. Flowlu abre um iframe com sua página de conexão e envia um POST:
{ domain, account.id, app.id, auth.access_token, ... }
│
▼
3. Sua página de conexão:
- solicita o nome do canal / outras configurações ao gerente
- POST https://{domain}/api/v1/module/contactcenter/bot/create
with Bearer {auth.access_token}
{ name, webhook_url, bot_token, active: 1 }
│
▼
4. Flowlu retorna: { data: { id, uuid, ... } }
│
▼
5. Você salva uuid (=bot_id) e id. Fecha o iframe via JS SDK.
13.2 Usuário digita, gerente responde
1. Usuário digita "Olá" no seu 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": "Olá", "send_date": 1710752400, "user_data": { "name": "John", "phone": "+1234567890" }, "attachments": [] } } ← 200 { "success": true } │ ▼ 3. Gerente responde "Olá! Como posso ajudar?" │ ▼ 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": "Olá! Como posso ajudar?", "timestamp": 1710752700, "event_id": "<token>", "attachments": [] } } ← 200 (você responde imediatamente) │ ▼ 5. Você entrega a mensagem ao usuário, recebe seu 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. O status da mensagem do gerente muda de "enviando" → "entregue".
13.3 Gerente escreve primeiro
1. Gerente clica em "Escrever primeiro" (disponível se 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 sua consulta..." },
"event_id": "<token>"
}
}
← 200
│
▼
3. Você encontra o usuário por telefone, abre o chat com external_chat_id=chat_99,
entrega a mensagem
│
│ se falhar — 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 sua consulta...",
"send_date": 1710752800,
"user_data": { ... },
"direction": 1
}
}
← 200
│
▼
5. Uma conversa com external_chat_id=chat_99 é criada no Flowlu. O gerente vê o chat.
14. Exemplos de código
Placeholders usados nos exemplos:
- {domain} — my.flowlu.com (valor do POST do iframe);
- {account_id} — 123456;
- {bot_id} — 550e8400-e29b-41d4-a716-446655440000 (UUID do canal);
- {access_token} — Token OAuth2 do usuário.
14.1 curl: criar um canal
curl -X POST 'https://{domain}/api/v1/module/contactcenter/bot/create' \
-H 'Authorization: Bearer {access_token}' \
-H 'Content-Type: application/json' \
-d '{
"name": "Meu 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 uma nova mensagem
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": "Olá!",
"send_date": 1710752400,
"external_message_id": "msg_001",
"external_user_id": "user_42",
"external_chat_id": "chat_42",
"attachments": [],
"user_data": { "name": "John Doe", "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 webhook e 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',
'Hello!',
['name' => 'John Doe', 'phone' => '+1234567890']
);
FAQ
Posso usar uma webhook_url para múltiplos canais?
Sim. Diferencie-os pelo channel_id (= bot_token definido durante a criação de cada canal).
O que devo passar no bot_token — algum segredo fixo?
É o identificador da integração no seu lado, não um segredo. É conveniente usar seu próprio UUID ou o ID do canal do seu banco de dados. O importante é que seja único dentro do seu aplicativo e imutável.
O que devo passar em user_data para uma mensagem de eco do gerente (direction=1)?
Os dados do interlocutor (cliente), não do gerente. O CRM usa isso para vincular ao perfil do cliente.
O que devo passar em external_user_id para uma mensagem de saída do gerente de outro dispositivo?
O ID do interlocutor, não do remetente. A mensagem é associada ao chat onde esse interlocutor está se comunicando em qualquer caso.
A mensagem não aparece no Flowlu — o que devo verificar?
- O canal está ativo (active = 1)?
- A URL da solicitação está correta: /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}?
- A resposta é 200 { "success": true }? 400 — payload inválido, 404 — canal/mensagem incorreta, 409 — canal desativado, 410 — canal excluído.
- O external_message_id é único dentro da conversa? Duplicatas são ignoradas.
O gerente digita, mas meu webhook não é chamado, por quê?
- A webhook_url está definida no canal? Verifique via solicitação de API para ler o canal.
- A URL responde em um tempo razoável (alguns segundos) e com um código 2xx?
- A URL é HTTPS e acessível publicamente?
- O limite de desativação automática foi atingido? Se o servidor respondeu com erros 10 vezes, o canal é desativado e aguarda ativação manual.
O que devo fazer se receber um webhook de saída duplicado?
Construa seu processamento de forma idempotente: para message.new.personal, use o event_id — se o evento já tiver sido processado, retorne 200 OK sem realizar novas ações.
Posso alterar a webhook_url de um canal existente?
Sim, através de uma solicitação de API para atualizar o canal com uma nova webhook_url.
Onde posso obter o account_id do cliente?
Na solicitação POST para o iframe, no campo account.id. Este é um identificador de conta público usado nas URLs de webhook de entrada.
Onde posso obter o domínio do cliente?
Na solicitação POST para o iframe, no campo domain. Use-o como a URL base para todas as solicitações de API para essa conta.
Os recibos de leitura ("mensagem lida") são suportados?
Não. O método message.read.personal está reservado, mas não é processado na versão atual.
- attach_file incoming_events.yaml (31.87KB)
- attach_file outgoing_hooks.yaml (16.18KB)