1. Flowlu
  2. Flowlu Central de ajuda
  3. Guia de desenvolvimento de aplicativos do Marketplace
  4. Desenvolvimento de um aplicativo de módulo de comunicações

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

  1. 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".
  2. Implementar uma página de conexão — ela abre em um iframe dentro do Flowlu quando um gerente configura a integração.
  3. Implementar um receptor de webhook de saída em seu servidor para lidar com as solicitações do Flowlu.
  4. 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:

CanalIniciado PorPropósito
Página de conexão em iframeFlowlu abre em um iframe, realiza um POSTO usuário configura a integração no Flowlu; seu aplicativo recebe o contexto do usuário (incluindo auth.access_token)
API do FlowluSeu servidorCriar/atualizar/excluir um canal, enviar webhooks de entrada, relatar erros
Webhooks de SaídaFlowluEntregar 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

TermoDescrição
Aplicativo ExternoSeu código — lado do servidor + página de conexão em iframe
Manifesto do AplicativoConfiguraçã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.idO UUID do seu aplicativo no Flowlu. Enviado na solicitação POST para o iframe
account.idID numérico da conta do Flowlu onde o canal está sendo conectado. Enviado no POST para o iframe
domainO 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_tokenToken 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_tokenIdentificador 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_idIdentificador 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_idIdentificadores para o chat / mensagem / usuário em seu sistema
inner_message_idIdentificador 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:

  1. 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".
  2. 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.
  3. Implemente um receptor de webhook de saída em seu servidor em uma URL persistente (você especificará esta URL ao criar o canal).
  4. Crie um canal de comunicação via POST /api/v1/module/contactcenter/bot/create em nome do usuário que abriu o iframe.
  5. 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

  1. Vá para https://{your-domain}/module/miniapps/cabinet.
  2. Clique em Criar aplicativo, preencha os campos principais e salve.
  3. 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:

CampoDescrição
idIdentificador arbitrário do ponto de integração dentro do seu manifesto
HeaderO 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
ActionComo a página de conexão abrirá: janela modal, painel lateral ou nova aba
TitleTítulo da página aberta (se janela modal ou painel lateral for selecionado)
SizeTamanho 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": ""
}
CampoDescrição
domainDomínio da conta Flowlu. Use como URL base: https://{domain}/api/v1/...
languageIdioma da interface do usuário
placement.idID do ponto de integração do seu manifesto
placement.codeCódigo do ponto de integração. Para o Contact Center — sempre contactcenter.service.wizard
account.idID numérico da conta Flowlu. Usado nas URLs de webhook de entrada
app.idUUID do seu aplicativo
app.versionVersão atual do manifesto (relevante para apps públicos)
auth.access_tokenToken OAuth2 do usuário que abriu o iframe
auth.refresh_tokenToken de atualização do usuário
auth.expires_atTempo de expiração do access_token
ratelimit.limitLimite de solicitações de API
bot_idUUID 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
}
CampoTipoObrigatórioDescrição
namestringsimNome do canal exibido para o gerente no Contact Center. Geralmente definido pelo gerente na página de conexão
webhook_urlstringsimURL HTTPS do seu servidor para receber webhooks de saída
bot_tokenstringsimIdentificador 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
activeint / boolnão1 — ativo (padrão), 0 — desativado. Um canal desativado não aceita hooks de entrada (responde com 409)
can_write_firstbooleannãotrue — 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

HTTPCorpoSignificado
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
410vazioCanal 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"
    }
  }
}
CampoTipoObrigatórioDescrição
textstringnão (padrão "")Texto da mensagem
send_dateintnão (padrão time())Unix timestamp (segundos)
external_message_idstringsimID da mensagem no seu sistema. Usado para deduplicação
external_user_idstringsimID do usuário interlocutor no seu sistema
external_chat_idstringsimID do chat no seu sistema
directionintnão0 — entrada (cliente → gerente), valor padrão. 1 — saída (eco de uma mensagem do gerente enviada fora do Flowlu)
attachmentsarraynãoLista de anexos
user_dataobjectnãoDados 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"
  }
}
CampoTipoObrigatórioDescrição
external_message_idstringsimID da mensagem sendo editada
edited_dateintnão (padrão time())Unix timestamp da edição
new_textstringnã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
  }
}
CampoTipoObrigatórioDescrição
external_message_idstringsimID da mensagem sendo excluída
deleted_dateintnãoUnix 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"
  }
}
CampoTipoObrigatórioDescrição
inner_message_idintsimID da mensagem no Flowlu (enviado no message.new.personal de saída)
external_message_idstringsimID 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"
  }
}
CampoTipoObrigatórioDescrição
event_idstringsimO valor event_id do webhook de saída original — retornado exatamente como recebido
messagestringnãoTexto 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:

  1. O Flowlu realiza um POST para sua webhook_url.
  2. Você responde com 2xx assim que aceitar a solicitação para processamento. A resposta significa apenas "aceito".
  3. Em seguida, você entrega a mensagem ao usuário no seu lado (isso pode levar qualquer tempo).
  4. 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:

TentativaQuando
1Imediatamente
20,5s após a falha da Tentativa 1
31s 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": []
  }
}
CampoTipoDescrição
channel_idstringSeu bot_token fornecido durante a criação do canal. Use-o para identificar a integração
inner_message_idstringID da mensagem no Flowlu. Salve-o — você precisará dele para o message.completed.personal
external_chat_idstringID do chat no seu sistema (você o enviou no message.new.personal)
textstringTexto da mensagem
timestampintUnix timestamp do envio
event_idstringToken de evento opaco. Necessário apenas se você enviar uma resposta de erro
attachmentsarrayLista de anexos

O que fazer:

  1. Responda com 200 imediatamente.
  2. Encontre o chat por external_chat_id e entregue a mensagem ao usuário.
  3. Após a entrega — envie message.completed.personal com inner_message_id e seu external_message_id.
  4. 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>"
  }
}
CampoTipoDescrição
channel_idstringSeu bot_token
toobjectDestinatário
message.textstringTexto da primeira mensagem
event_idstringToken de evento opaco

O que fazer:

  1. Responda com 200 imediatamente.
  2. Encontre/crie o usuário no seu lado usando os campos to (priorize conforme sua necessidade: telefone → e-mail → outro → nome).
  3. Abra um chat com este usuário e obtenha seu external_chat_id.
  4. Entregue message.text ao usuário.
  5. Envie message.new.personal com este external_chat_id e direction: 1 (eco do gerente) — isso cria o chat no Flowlu.
  6. 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:

ValorDescrição
manualAdministrador da conta desativou manualmente o canal
auto_unreachableFlowlu desativou automaticamente o canal devido a uma webhook_url consistentemente inacessível (10 falhas consecutivas)
subscription_expiredA 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"
}
CampoDescrição
nameNome completo. Dividido em nome/sobrenome pelo primeiro espaço
usernameApelido/login no seu sistema
phoneNúmero de telefone
emailEndereço de e-mail
avatar_urlURL do avatar (será baixado)
public_linkLink 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
}
CampoTipoObrigatórioDescrição
idstringsimID exclusivo do anexo no seu sistema
urlstringsimURL HTTPS para download. Deve estar acessível para o servidor do Flowlu
filenamestringsimNome do arquivo com extensão
typestringnão (padrão file)Tipo: photo, file, video, audio, location
sizeintnãoTamanho em bytes
metadataobjectnãoObjeto 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"
}
CampoDescrição
phoneNúmero de telefone do destinatário em formato internacional
emailEndereço de e-mail do destinatário
nameNome do destinatário (quando telefone/e-mail não estiverem disponíveis)
otherIdentificador arbitrário em seu sistema

11. Limites

ParâmetroValor
Canais MiniApp por conta10
Número de anexos em uma mensagem10
Tamanho máximo do arquivo50 MB
Tentativas de entrega de webhook de saída3
Limite para desativação automática10 falhas consecutivas
Esquema de webhook_url permitidoApenas 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?

  1. O canal está ativo (active = 1)?
  2. A URL da solicitação está correta: /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}?
  3. A resposta é 200 { "success": true }? 400 — payload inválido, 404 — canal/mensagem incorreta, 409 — canal desativado, 410 — canal excluído.
  4. O external_message_id é único dentro da conversa? Duplicatas são ignoradas.

O gerente digita, mas meu webhook não é chamado, por quê?

  1. A webhook_url está definida no canal? Verifique via solicitação de API para ler o canal.
  2. A URL responde em um tempo razoável (alguns segundos) e com um código 2xx?
  3. A URL é HTTPS e acessível publicamente?
  4. 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.

Previous Desenvolvimento de um aplicativo de módulo de telefonia
Next Perguntas frequentes do Marketplace do Flowlu