> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zapmeow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ligação por voz

> Ligar pelo WhatsApp com o agente da ElevenLabs (ou com um humano pelo navegador), por sessão.

A ligação roda **no mesmo client** da sessão: sem segundo aparelho e sem segundo QR. Ela funde o sidecar de ligação do Sistema e o `voz-bridge` (ElevenLabs). Com ela ligada, o `voz-bridge` pode ser desligado.

> [!WARNING]
>
> Desligada por padrão, em duas camadas. Sem `ZAPMEOW_VOICE=1` o meowcaller nem é montado no client. Com a variável, só as sessões com `enabled: true` o recebem (ele intercepta os eventos de chamada, então as outras ficam como estavam). Habilitar uma sessão que já está no ar pede `stop` e `start` dela (a resposta do `PUT /call/config` traz `reiniciar: true`). Ligação por WhatsApp não oficial tem risco de banimento: use número de teste até o OK.

## Ligar numa sessão

```bash theme={null}
# 1. liga a sessão (chave de admin), com teto, janela e lista de teste
curl -X PUT $URL/api/teste-entrega/call/config -H "X-Api-Key: $ZAPMEOW_API_KEY" \
  -d '{"enabled":true,"dailyCap":5,"allowed":["5511999990981"],"agentId":"agent_..."}'

# 2. gera o token de ligação da sessão (aparece só nesta resposta)
curl -X POST $URL/api/teste-entrega/call/token -H "X-Api-Key: $ZAPMEOW_API_KEY"

# 3. liga
curl -X POST $URL/api/teste-entrega/call -H "Authorization: Bearer $TOKEN_DA_SESSAO" \
  -d '{"phone":"5511999990981","callId":"<id da voiceCalls no Convex>","agentId":"agent_..."}'
# 202 {"callId":"...","bridgeCallId":"..."}
```

## Rotas

Aceitam a chave de admin **ou** o token da sessão. As de configuração, só a chave de admin. Sem `{sessão}` no caminho (`POST /api/call`), vale `?session=`, o campo `session` do corpo ou a única sessão.

| Rota | Corpo | Resposta |
| - | - | - |
| `POST /api/{sessão}/call` | `{phone, callId?, agentId?, extra?, mode?}` | `202 {callId, bridgeCallId}` |
| `POST /api/{sessão}/hangup` | | `{ok:true}`; `404` sem chamada |
| `POST /api/{sessão}/mute` | `{on}` | silencia o microfone do navegador (modo `browser`) |
| `GET /api/{sessão}/call/status` | | estado, chamada atual com `latencia`, `hoje`, `teto`, `habilitada`, `dentroDaJanela` |
| `GET /api/{sessão}/call/audio` | | WebSocket, `?token=`: `int16` LE mono 16 kHz, 1920 bytes por quadro, nos dois sentidos; texto do servidor = fase da chamada |
| `GET /api/{sessão}/call/config` | | `{config, tokenSet, elevenKeySet, eventTokenSet, hoje}` (nunca o token, a chave nem o token de eventos) |
| `PUT /api/{sessão}/call/config` | `enabled, dailyCap, windowStart, windowEnd, timezone, allowed, agentId, eventUrl` e, só de escrita, `elevenKey`, `eventToken` (parcial; `""` apaga o segredo) | config gravada |
| `POST /api/{sessão}/call/token` | | `{token}`: gera ou rotaciona; o anterior deixa de valer; o banco guarda só o hash |

`mode`: `eleven` (a IA atende; padrão quando vem `agentId`, `extra` ou `callId`, que é o contrato do `voz-bridge`) ou `browser` (um humano fala pelo `/call/audio`). `extra` (objeto JSON) vira as variáveis dinâmicas do agente; o agente vem de `agentId` do pedido, `extra.agentId` ou `agentId` da sessão, nessa ordem; só o tenant Glow cai em `ELEVENLABS_AGENT_ID`.

## Agente, chave e eventos por instância

Cada sessão guarda o próprio `agentId` e `eventUrl` (https; http só em localhost) na config. A chave da ElevenLabs (`elevenKey`) e o token dos eventos (`eventToken`) vão para o **cofre do tenant** da sessão, cifrados, sob os nomes `elevenlabs_api_key` e `voz_event_token`, e valem para todas as sessões do tenant. Eles só entram: nenhuma resposta nem log devolve o valor, o `GET` mostra só `elevenKeySet` e `eventTokenSet`. Precisa de `ZAPMEOW_MASTER_KEY` (sem cofre o `PUT` com segredo devolve 503). No painel admin, em **Voz e IA**, cada sessão do tenant tem um cartão com esses campos (também por `PUT /admin/tenants/{id}/s/{sessão}/call/config`); o portal do cliente não alcança essa rota.

Sessão sem agente, ou sem chave do próprio tenant, não liga: `422` com `sem agente ElevenLabs...` ou `sem chave ElevenLabs...`, antes de discar e sem gastar o teto. Nunca cai na chave de outro tenant. As variáveis `ELEVENLABS_API_KEY`, `ELEVENLABS_AGENT_ID`, `VOZ_EVENT_URL`, `VOZ_EVENT_TOKEN` e `VOZ_TOKEN` são só o recuo do tenant Glow (a chave e o token gravados no cofre dele vencem o env).

## Códigos

| Código | Quando |
| - | - |
| `400` | JSON ou `phone` inválido |
| `401` | sem credencial, token errado ou de outra sessão |
| `403` | número fora de `allowed` ou fora da janela |
| `422` | modo `eleven` sem agente; número sem WhatsApp |
| `423` | ligação desligada na sessão |
| `429` | teto diário atingido (vem **antes** do `409`) |
| `409` | já existe chamada em andamento na sessão |
| `502` | falha ao discar |
| `503` | `ZAPMEOW_VOICE` desligada, ligação não montada na sessão (falta stop/start) ou WhatsApp desconectado |

## Controles

* **Teto diário** (`dailyCap`, padrão 20 ou `ZAPMEOW_VOICE_TETO`): contado no Postgres (`zm_voice_usage`), atômico, vira à meia-noite do fuso da sessão. `409` e `422` não gastam o teto. `0` bloqueia tudo.
* **Janela** (`windowStart`/`windowEnd`, padrão 08:00 às 21:00 em `America/Sao_Paulo`): fora dela, `403`. Os dois vazios = sem janela. Início maior que o fim vale pela madrugada.
* **Lista de teste** (`allowed`): vazia = qualquer número; com números, só eles.
* **Uma chamada por vez** por sessão; teto de 15 minutos e 90 s sem resposta.

## Aviso de andamento ao Convex

Com `eventUrl` na config da sessão (ou `VOZ_EVENT_URL`, só no tenant Glow) e `callId` no pedido, o ZapMeow avisa o Convex como o `voz-bridge`: `POST` com `Authorization: Bearer <eventToken do cofre do tenant>` (no tenant Glow, o recuo é `$VOZ_EVENT_TOKEN` ou `$VOZ_TOKEN`; sem token, sem o cabeçalho) e corpo `{callId, event, error}`. Eventos: `ringing`, `answered`, `ended`, `rejected`, `no_answer`, cada final uma vez só. A IA entra no aceite do par, sem esperar o primeiro pacote de áudio.

Para o Convex trocar o `voz-bridge` pelo ZapMeow, basta apontar `VOZ_BRIDGE_URL` para `https://<zapmeow>/api/<sessão>` e `VOZ_BRIDGE_TOKEN` (é o nome que o Convex lê) para o token da sessão. `scripts/trocar-voz-convex.sh` faz a troca e a reversão.

## Áudio do lead e latência

A voz do lead passa pelo RNNoise (rede neural, cgo) antes de ir ao agente: ruído de fundo não vira interrupção. `VOZ_NOISE_GATE=0` desliga e manda o áudio cru. A latência (fim da fala do lead até o 1º áudio da IA na chamada) sai no log (`latência: N ms`) e em `call/status` (`chamada.latencia`: `amostras`, `mediaMs`, `minMs`, `maxMs`, `ultimasMs`). Para comparar com o `voz-bridge` na mesma chamada, use `scripts/ab-latencia.mts`.

## Variáveis

| Variável | Uso |
| - | - |
| `ZAPMEOW_VOICE` | `1` liga o recurso (padrão desligado) |
| `ZAPMEOW_VOICE_TETO` | teto diário padrão de sessões novas (20) |
| `ZAPMEOW_VOICE_ORIGINS` | origens extras aceitas no WebSocket de áudio (além de localhost) |
| `ELEVENLABS_API_KEY` | recuo só do tenant Glow: chave da ElevenLabs (os demais usam `elevenKey` do cofre do próprio tenant) |
| `ELEVENLABS_AGENT_ID` | recuo só do tenant Glow: agente padrão quando a sessão e o pedido não trazem |
| `VOZ_EVENT_URL`, `VOZ_EVENT_TOKEN` | recuo só do tenant Glow: aviso de andamento ao Convex (os demais usam `eventUrl` e `eventToken`) |
| `VOZ_NOISE_GATE` | `0` desliga o RNNoise |

O agente da ElevenLabs precisa estar configurado com áudio `pcm_16000` na entrada e na saída.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.