Como os dois serviços se conectam para transformar uma chamada SIP em uma conversa conduzida por bots de IA — multi-tenant, multi-trunk, com um orquestrador roteando cada chamada para o agente certo.
Três camadas independentes, cada uma com uma responsabilidade clara. A chamada flui de baixo (rede SIP) para cima (lógica de negócio) e volta como áudio.
Servidor que fala SIP/RTP com o provedor (Vono) e expõe uma API estilo
Asterisk ARI (REST + WebSocket). É quem registra os troncos, recebe/origina chamadas e
entrega o áudio. Multi-tenant via clients_dir: um JSON por cliente.
Serviço que conversa com o ksir via ARI. Faz STT (fala→texto), TTS
(texto→fala), detecção de voz (VAD/barge-in) e, a cada turno, chama o webhook de negócio.
Expõe a API POST /api/v1/calls para disparar chamadas.
Webhooks que decidem o que falar. O orquestrador
recebe o início da chamada e escolhe o agente. Os agentes
(cobrancabot, apresentadorbot) conduzem a conversa turno a turno. Tudo no mesmo runtime JS,
com contexto do lead carregado de arquivos por external_id.
Plataforma × regra de negócio. O ksir e o voicemw são a plataforma — infraestrutura genérica de telefonia e middleware, comum a todos os clientes. Já os scripts de orquestrador, cobrador e apresentador são regras de negócio: são desenvolvidos sob medida de acordo com cada empresa contratante — seu roteiro de conversa, seus critérios de roteamento, sua escada de desconto, seu produto. Trocar de cliente significa trocar (ou customizar) esses scripts, sem mexer na plataforma.
O painel administrativo também é sob medida. O site que administra os scripts de regra de negócio e exibe as informações e o status da telefonia (chamadas, campanhas, acordos, métricas) é customizado conforme a necessidade de cada cliente. Por exemplo: se o contratante é uma cobradora, o painel é construído segundo as diretrizes e parâmetros dela — telas de carteira de devedores, acompanhamento de acordos fechados, funil de negociação. Outro cliente, com outra operação, terá um painel modelado para a realidade dele. A plataforma (ksir + voicemw) permanece a mesma; o que muda é a camada de gestão e visualização.
O caminho completo de uma chamada outbound, do disparo via API até a voz do agente, e a volta do áudio do interlocutor.
POST /api/v1/calls no voicemw com destino, external_id, outbound_trunk (opcional) e initial_text.PJSIP/<destino>@<trunk>, no app voicemw-vmtech.slin16 16 kHz.started → orquestradorAo entrar em Stasis, voicemw chama o webhook principal do tenant (o orquestrador), que lê os parâmetros e decide o agente.user_speech), voicemw chama o webhook do agente escolhido. O agente responde com comandos (play_audio, hangup, transfer).O ksir tem uma config global e um arquivo JSON por cliente (tenant) dentro de
clients_dir. Cada cliente lista seus troncos SIP. Não há SIGHUP —
para adicionar/editar clientes, reinicie o serviço.
{
"ari_listen": "127.0.0.1:7077", // API estilo ARI (REST + WebSocket)
"sip_host": "31.220.97.96", // IP público que fala com a Vono
"sip_port": 5060,
"rtp_port_start": 30000,
"rtp_port_end": 30999,
"clients_dir": "./clients" // 1 arquivo .json por tenant
}
Um tenant agrupa todos os troncos SIP de um mesmo cliente. Aqui o vmtech
tem dois troncos da Vono — um por atividade. Cada tronco tem auth_user,
dids e name únicos globalmente (o ksir valida no boot).
{
"tenant": "vmtech",
"stasis_app": "voicemw-vmtech", // app que recebe as chamadas no voicemw
"trunks": [
{
"name": "vmtech_vono", // COBRANÇA
"auth_user": "ajvy90872",
"server": "sip.vono2.me",
"dids": ["554428880123"]
},
{
"name": "ksir_vono", // APRESENTAÇÃO
"auth_user": "rgjb82929",
"server": "sip.vono2.me",
"dids": ["554420181470"]
}
]
}
No boot o ksir registra os dois troncos na Vono (REGISTER ok) e injeta os channelvars
TENANT, TRUNK_NAME e X_CID em cada chamada — é assim que o voicemw sabe a qual tenant/tronco ela pertence.
O voicemw é multi-tenant num único processo. Cada tenant tem seu stasis_app, trunk de saída, token de autenticação, webhook (o orquestrador) e a lista de agentes.
"tenants": [{
"id": "vmtech",
"stasis_app": "voicemw-vmtech",
// tronco default + secundários (allowlist p/ outbound_trunk no POST)
"outbound_trunk": "vmtech_vono", // usado quando o POST não pede trunk
"outbound_trunks": ["ksir_vono"], // SIPs secundários escolhíveis por chamada
"auth_token": "<openssl rand -hex 32>", // Bearer da API de disparo
"caller_id": "Empresa <554428880123>",
// webhook principal = ORQUESTRADOR (recebe o 'started')
"webhook": { "url": "https://teste.flexpage.io/orquestrador" },
// cada agente = um webhook próprio; id casa com o retorno do orquestrador
"agents": [
{ "id": "cobranca", "url": "https://teste.flexpage.io/cobrar" },
{ "id": "apresentacao", "url": "https://teste.flexpage.io/apresentar" }
]
}]
| Campo | Papel |
|---|---|
outbound_trunk | Tronco default de saída. Usado quando o POST não especifica trunk. |
outbound_trunks | Allowlist dos SIPs secundários escolhíveis por chamada. O default não se repete aqui (já entra automático). |
auth_token | Bearer que autentica o POST /api/v1/calls e resolve o tenant. |
webhook.url | O orquestrador. Recebe o evento started de toda chamada do tenant. |
agents[].id | Identificador do agente. Deve bater exatamente com o agent_id devolvido pelo orquestrador. |
agents[].url | Webhook do agente. Recebe os eventos da chamada a partir do 1º user_speech. |
Os globais STT (Google v2 telephony pt-BR, projeto flexhook-82bac), TTS (Google Neural2-C), audio_capture (VAD agressividade 2, RTP 25000–25999, slin16) e originate_api (HTTPS 0.0.0.0:7090) ficam fora do bloco do tenant e valem para todos.
O webhook principal do tenant é o orquestrador. Ele só atua no started:
lê os parâmetros de entrada e responde com o agent_id que deve conduzir a chamada.
A partir daí, o voicemw roteia tudo direto para o webhook do agente.
Três camadas de prioridade. A primeira que casar vence:
| Prioridade | Critério | Exemplo → agente |
|---|---|---|
| 1 | Prefixo do external_id | FAT-/COB-/DIV- → cobranca · LEAD-/APR- → apresentacao |
| 2 | DID próprio da chamada | 554428880123 → cobranca · 554420181470 → apresentacao |
| 3 | Fallback | → cobranca (default) |
{
"call_id": "a96c9de3-…",
"agent_id": "apresentacao", // ← voicemw passa a rotear p/ este agente
"commands": [] // vazio → voicemw toca o initial_text do POST
}
O agente não recebe o evento started — o orquestrador o consome. Por isso
cada agente recarrega o contexto do lead de forma lazy no primeiro user_speech
(if (!sess.devedor) recarrega…). Os dois bots já fazem isso. Consequência prática:
a saudação inicial vem do initial_text do POST — que deve ser LGPD-safe (só
nome + confirmação de identidade, sem valor da dívida).
Sequência real de uma chamada de apresentação, validada em log
(call a96c9de3).
external_id: LEAD-…, outbound_trunk: ksir_vono, initial_text com a saudação.PJSIP/5544991474799@ksir_vono, trunk_override aplicado, caller 554428880123.slin16 estabelecida.DECISÃO: agente="apresentacao" (external_id_prefix, LEAD-) → responde agent_id.agente: delegado pelo orquestrador → apresentacao. Toca o initial_text.play_audio (vira TTS), e ao fim hangup ou transfer.Os dois fluxos usam o mesmo endpoint e o mesmo Bearer. Muda o
outbound_trunk, o prefixo do external_id e o initial_text.
curl -sk -X POST https://pbx-01.flexpage.io:7090/api/v1/calls \
-H "Authorization: Bearer $TOKEN_VMTECH" \
-H "Content-Type: application/json" \
-d '{
"destination": "5544991474799",
"external_id": "FAT-2025-0042",
"initial_text": "Olá, aqui é uma ligação para Fulano. É com você que estou falando?"
}' # sem outbound_trunk → usa o default vmtech_vono
curl -sk -X POST https://pbx-01.flexpage.io:7090/api/v1/calls \
-H "Authorization: Bearer $TOKEN_VMTECH" \
-H "Content-Type: application/json" \
-d '{
"destination": "5544991474799",
"external_id": "LEAD-2026-ALERTME-00001",
"outbound_trunk": "ksir_vono",
"initial_text": "Oi, tudo bem? Aqui é sobre o Alertme. Posso te falar rapidinho?"
}'
| Campo do POST | Efeito |
|---|---|
destination | Número de destino (o telefone do lead/devedor). |
external_id | Identifica o lead e roteia o agente pelo prefixo. Precisa ter JSON de contexto correspondente. |
outbound_trunk | Escolhe o SIP de saída. Omitido → tronco default. Deve estar na allowlist do tenant. |
initial_text | Primeira fala (TTS). Como o agente não vê o started, é aqui que mora a saudação — mantenha LGPD-safe. |
openssl rand -hex 32 para cada tenant. Trocar o placeholder antes de expor a API.external_id disparado precisa de outbound_contexts/<external_id>.json — senão cai em fallback/encerramento.agent_id do orquestrador devem existir em agents[]. Divergência → agent_id desconhecido.outbound_trunks devem existir como trunks[].name no JSON do tenant.