Chamadas de voz

Instâncias calls, discagem, eventos e áudio pelo painel.

Ligações usam uma instância de tipo calls, com pareamento próprio e áudio nos dois sentidos pelo navegador. O módulo precisa estar habilitado no servidor. Vídeo é opcional por cima do áudio, habilitado à parte e ainda experimental (veja Chamadas de vídeo).

Capacidade anunciada

Consulte GET /v1/capabilities com admintoken. Exigir capabilities.calls=true e instance_kinds contendo calls. Este recorte ilustra um servidor habilitado:

json
{
  "capabilities": {"calls": true, "calls_per_instance_limit": true, "calls_video": false},
  "instance_kinds": ["whatsapp", "calls"],
  "calls_max_concurrent": 1,
  "calls_max_concurrent_range": {"min": 1, "max": 8}
}

Com ATENDROZAP_CALLS_ENABLED=false, criar ou operar a instância de ligação é recusado. O limite padrão é uma chamada por instância. Conectar uma instância de mensagens não habilita voz nela. calls_video anuncia o vídeo (ATENDROZAP_CALLS_VIDEO_ENABLED=true); sem ele, video: true e as rotas de vídeo respondem 501 calls_video_not_supported.

Limite de ligações por instância

Escolha Ligações simultâneas ao criar uma instância de ligação ou abra Instância → Visão geral → Ligações simultâneas → Salvar limite. Aceita de 1 a 8, incluindo ligações ainda tocando. O único modo homologado em chamadas reais continua sendo 1 por vez.

Pela API, informe callsMaxConcurrent em POST /instance/init com kind: calls. Para alterar depois, use POST /instance/call-settings com o header token, ou POST /v1/instances/{id}/call-settings com admintoken:

json
{"callsMaxConcurrent": 2}

O limite é persistido e aparece no objeto instance do status e da listagem, junto de callsConfigVersion (inteiro crescente; inclusive 0 antes de uma edição). Voz e vídeo compartilham o mesmo limite; cada chamada ringing, connecting ou active, recebida ou realizada, ocupa uma vaga. Pode ser salvo com a instância desconectada. Alterar não reconecta a sessão: ligações em andamento são preservadas, mesmo que ultrapassem um limite reduzido; novas ligações aguardam haver vaga e recebem 409 line_busy enquanto não houver. O worker local aplica imediatamente. Se a sessão estiver em outro worker, ele aplica na próxima renovação de lease e antes de cada nova ligação de saída.

calls_max_concurrent em capacidades é o padrão do servidor, não o limite individual. Instâncias antigas sem escolha própria continuam usando esse padrão até a primeira edição; novas instâncias persistem o padrão da criação.

Configure os eventos call, call_settings e connection na aba Webhook da própria instância. A inscrição também pode ser feita por API, conforme o guia de webhooks.

Cada POST /instance/call-settings ou variante administrativa bem-sucedido incrementa callsConfigVersion e grava um evento call_settings na mesma transação, mesmo quando o valor enviado é igual ao anterior ou a instância está desconectada. O evento é entregue somente com inscrição explícita em call_settings; mudar o limite não altera a inscrição do webhook. O novo evento mantém o envelope assinado existente, com data: {kind: "calls", callsMaxConcurrent, callsConfigVersion, updatedAt}. updatedAt usa UTC em RFC3339Nano; nos objetos HTTP o campo de data existente continua se chamando updated.

capabilities.calls_settings_webhook=true anuncia esse contrato. Eventos connection de uma instância calls também acrescentam instance.kind e, quando o banco pode ser consultado, instance.callsMaxConcurrent e instance.callsConfigVersion. Esses dois últimos campos são opcionais: a ausência não redefine o limite. Assinar o webhook não envia um snapshot inicial; consulte GET /instance/status ao integrar ou retomar a instância e compare a versão antes de aplicar respostas HTTP ou eventos, evitando regressões.

Uma instância calls gera eventos de chamada (call), configuração (call_settings) e ciclo de vida (connection, limits, conforme inscrição). Ela não persiste nem publica mensagens, recibos, edições, eventos de grupos, histórico de mensagens ou undecryptable recebidos nessa sessão. Uma instância whatsapp separada continua responsável pelas conversas.

O prompt de integração com o Atendro detalha sincronização, vagas, reservas e testes. As adições de configuração e o filtro de mensagens descritos aqui aguardam publicação; não presumir que uma API em produção já os anuncia.

Primeiro teste pelo painel

  1. Abra o servidor no console e clique em Nova instância.
  2. Selecione Tipo → Ligação, informe um nome e crie. O painel guarda o token no backend.
  3. Solicite a conexão e escaneie o QR pelo WhatsApp do número autorizado. Aguarde connected. Pode ser outro dispositivo vinculado do mesmo número usado para mensagens ou um número dedicado.
  4. Nas Conversas da instância de mensagens, abra a conversa autorizada e clique em Ligar. O painel escolhe uma instância de ligação conectada do mesmo servidor cujo token conhece. Também é possível usar Discar na página da instância de ligação.
  5. Permita o microfone, atenda no telefone de destino e confira áudio nos dois sentidos. O widget segue a tela de ligação do WhatsApp: o contato no palco, os controles redondos embaixo. Mudo controla o microfone; Câmera liga o vídeo (quando habilitado); Gravando mostra a gravação automática e, ao encerrar, vira Gravação, que abre o player do áudio ali mesmo; Desligar encerra a chamada. Mantenha a página aberta durante a conversa.

Se o tipo Ligação não aparecer, confira a capacidade do servidor. Se o painel não conhecer o token de uma instância criada fora dele, reemita-o pela interface antes do uso. Erros de linha ocupada ou sessão desconectada são mostrados no widget.

Ruído e eco

O painel solicita cancelamento de eco, supressão de ruído e ajuste de ganho ao navegador. Quando o dispositivo oferece o modo de cancelamento de todo o áudio reproduzido, ele é preferido para incluir a voz que sai pelo painel. O widget avisa quando não consegue confirmar o cancelamento de eco.

Reduzir ruído do microfone vem marcado: acrescenta um filtro de graves e atenua suavemente o fundo entre as falas. Desmarque durante uma chamada para comparar, especialmente se sua voz for muito baixa. Essa opção controla o filtro adicional; o cancelamento de eco do navegador continua solicitado. O tratamento ocorre no computador do atendente antes do envio e não altera o som recebido. Quando a gravação automática está habilitada no servidor, ela captura a voz já tratada do atendente e a voz recebida do contato.

O filtro não elimina todas as vozes, teclas ou sons fortes enquanto você fala. Use fone se persistir eco; no teste com dois aparelhos próximos, afaste o telefone do microfone do computador e evite volume alto no viva-voz. A qualidade depende também do navegador, microfone e ambiente.

Rotas e estados

OperaçãoResultado
POST /call/makeCorpo {number, video?}; 201 com callId; aceita Idempotency-Key
POST /call/answerCorpo {callId}; atende uma chamada recebida
POST /call/rejectCorpo {callId}; recusa uma chamada recebida
POST /call/hangupCorpo {callId}; encerra a chamada
POST /call/videoCorpo {callId, action} com start, accept ou stop; liga, aceita ou desliga o vídeo
GET /call/activeLista as chamadas vivas da instância
GET /call/{id}Estado da chamada viva ou do histórico persistente
GET /call/historyHistórico paginado, com filtros e situação da gravação
GET /call/{id}/recordingReprodução/download autenticado do WAV, com suporte a Range
GET /call/{id}/audioUpgrade WebSocket; PCM s16le mono a 16 kHz
GET /call/{id}/videoUpgrade WebSocket; H.264 Annex-B com cabeçalho de 8 bytes e avisos JSON

Todas usam o token da instância calls. Estados: ringing, connecting, active, ended; o encerramento inclui endReason e endedBy. Uma instância whatsapp mantém 501 calls_not_supported em /call/make; outras rotas de ligação recusam o tipo incorreto com 409 wrong_instance_kind.

O áudio passa pelo console, que autentica a sessão do navegador e adiciona o token no pedido à API. Apenas as pontes de áudio e vídeo aceitam ?token= como alternativa ao header. Prefira o proxy para manter a credencial no backend e não registrar tokens em URLs. A perda do socket de áudio ou vídeo abre uma janela de 30 segundos para reconexão com o mesmo callId. Reabrir o socket do mesmo tipo cancela seu prazo; se ele não retornar, a chamada termina com hangup/local. A negociação de vídeo é preservada durante a janela e a nova ponte pede um keyframe. Para desligar voluntariamente, use POST /call/hangup; para desligar só a câmera, use POST /call/video com action: stop.

Chamadas de vídeo

Recurso experimental. O motor de chamadas transporta o vídeo (H.264 pelo relay do WhatsApp) e a sinalização de upgrade foi validada pelo projeto upstream contra uma segunda implementação, mas o caminho de mídia de vídeo ainda não foi homologado com o número de laboratório. Por isso o vídeo fica atrás de ATENDROZAP_CALLS_VIDEO_ENABLED e é anunciado em capabilities.calls_video.

Três formas de haver vídeo numa chamada:

  1. Chamada de vídeo desde o toque: POST /call/make com {"number": "...", "video": true}. A oferta anuncia vídeo e videoState.sending já vem verdadeiro.
  2. Upgrade no meio da chamada: POST /call/video com {"callId": "...", "action": "start"} numa chamada atendida. Fica videoState.pending até o contato aceitar no telefone; aceite abre o envio (sending), recusa devolve ao áudio. Numa chamada que já negociou vídeo, start apenas religa a câmera.
  3. Pedido do contato: o telefone pede vídeo e chega data.event: "video" com videoState.requested. Responda com action: "accept"; ignorar deixa o WhatsApp retirar o pedido. Uma chamada recebida como vídeo já nasce com os dois fluxos anunciados.

action: "stop" desliga a câmera deste lado (ou retira o pedido pendente) sem mexer no áudio nem no vídeo do contato. videoState.receiving diz se o contato está com a câmera ligada e orientation (0–3, quartos de volta) como girar os quadros dele.

Os quadros trafegam por GET /call/{id}/video, um WebSocket separado do áudio. Cada mensagem binária é um cabeçalho de 8 bytes (versão 1; flags com bit 0 = keyframe; orientação; reservado; duração do quadro em microssegundos, big-endian) seguido de uma unidade de acesso H.264 em Annex-B. A API não codifica nem decodifica pixels: quem envia produz H.264 (WebCodecs no navegador, ffmpeg, codec de hardware) e quem recebe decodifica. Abra o socket junto com a chamada, antes de haver vídeo, para não perder o keyframe que abre o fluxo do contato; quadros enviados com o fluxo fechado são descartados, e enquanto o motor espera um keyframe os quadros intermediários também. Mensagens de texto da API são avisos JSON: {"event":"keyframe"} pede um IDR no próximo quadro e {"event":"video","video":{...}} acompanha cada mudança de videoState (o primeiro aviso é o estado atual). Códigos de fechamento: 4000 chamada encerrada, 4001 cabeçalho inválido, 4002 falha de escrita, 4003 substituído por outro socket.

No painel, com a capacidade anunciada e um navegador com WebCodecs (Chrome, Edge, Safari 16.4+, Firefox 130+), o discador da instância de ligação ganha Chamada de vídeo, as conversas ganham Vídeo, e Câmera no widget faz o upgrade. O vídeo do contato ocupa o palco do widget e a própria câmera aparece como miniatura, como na tela de chamada do WhatsApp. Resolução enviada: até 640×480 a 15 quadros por segundo, cerca de 0,6 Mbit/s por sentido — o servidor só repassa os bytes, sem transcodificar.

O histórico marca video: true nas chamadas que tiveram vídeo. Com capabilities.calls_video_recording=true, a captura opcional também gera um MP4 com as duas câmeras lado a lado após o encerramento. O WAV continua independente; videoRecording informa o processamento e o download. Veja Gravação de videochamadas para habilitação, limites e integração no Atendro. A gravação de vídeo permanece desligada por padrão e ainda exige homologação em celulares reais.

Registrar o atendimento no sistema parceiro

Inscreva call no webhook global ou próprio para receber data com callId, direção, estado e motivo de encerramento. A entrega segue a política de assinatura, retry e deduplicação dos demais eventos.

O sinal de atendimento é EventType: "call" e data.event: "answered". Nesse momento data.answered é true, data.answeredAt traz o horário do aceite e data.status é connecting. O estado active chega depois, quando o motor recebe áudio; não espere por ele para registrar que atendeu. Isso vale para o aceite remoto de uma chamada realizada e para o atendimento local de uma chamada recebida. ringing e preaccept não são atendimento.

json
{
  "EventType": "call",
  "event": "call",
  "eventId": "ID_DO_EVENTO",
  "instanceId": "ID_DA_INSTANCIA",
  "seq": 2,
  "data": {
    "callId": "ID_DA_CHAMADA",
    "event": "answered",
    "direction": "outbound",
    "status": "connecting",
    "answered": true,
    "startedAt": "2026-09-18T15:00:00Z",
    "answeredAt": "2026-09-18T15:00:05Z"
  }
}

O exemplo é um recorte: o envelope completo inclui as identidades previstas no contrato. No consumidor, valide a assinatura sobre o corpo original, identifique a instância por instanceId e faça upsert por (instanceId, data.callId). Deduplicate por eventId; retries preservam esse identificador. Use seq do envelope para ordenar, pois data.seq pertence à sessão de ligações. Responda 2xx após guardar o evento.

Todo evento traz video (booleano) e, enquanto a chamada vive, videoState. data.event: "video" marca mudanças só de vídeo (pedido do contato, aceite, câmera ligada/desligada), sem alterar status.

data.event: "active" acrescenta mediaStartedAt, mantendo answeredAt. data.event: "ended" acrescenta endedAt, endReason, endedBy e durationSeconds. Os eventos posteriores também preservam answered=true e answeredAt, permitindo reconciliar o atendimento se o consumidor perdeu uma notificação. Chamadas não atendidas terminam com answered=false.

endReason: "hangup" com endedBy: "remote" também representa um nó WhatsApp terminate remoto sem motivo explícito (ausente, vazio ou só espaços). Indica solicitação remota de encerramento; isoladamente não comprova que alguém apertou o botão de desligar. Motivos explícitos são preservados; um callback genérico sem motivo continua unknown. O primeiro motivo fica no evento e no histórico, sem reclassificar chamadas antigas.

data.event: "recording_ready" informa callId e recording com status, bytes e durationMs depois de o arquivo ficar pronto. recording_failed informa falha, sem inventar um áudio disponível. Esses dois eventos atualizam a gravação, não o estado da chamada; não dependem de um campo status da chamada e podem chegar antes do evento de encerramento.

data.event: "video_recording_ready" e "video_recording_failed" informam callId e o objeto separado videoRecording, com contentType: "video/mp4", status, bytes, durationMs e failureReason. São atualizações da gravação de vídeo: não são ended, não mudam vagas de simultaneidade e não substituem recording do WAV. Consulte o guia de gravação de vídeo.

Histórico e gravação automática

No painel, abra a instância de ligação e a aba Histórico de chamadas. É possível filtrar realizadas/recebidas e atendidas/sem atendimento, consultar duração, ouvir e baixar. O histórico permanece após reiniciar a API e após a expiração do áudio; não depende da retenção de /v1/events.

Na API, GET /call/history?limit=25&answered=true lista a primeira página. Envie o nextCursor recebido como before para buscar chamadas mais antigas. Filtros opcionais: direction, status, from (inclusivo) e to (exclusivo), com datas RFC3339. Máximo de 100 itens por página.

Na VPS 1 a configuração solicitada é gravar todas as chamadas atendidas e reter o áudio por 30 dias. capabilities.call_history e capabilities.call_recording anunciam os recursos; a retenção está em calls_recording_retention_days. A captura começa no aceite, sem gravar toques, e acompanha substituições da ponte de áudio. O WAV tem PCM 16-bit, 16 kHz, dois canais: atendente à esquerda e contato à direita. O som processado ocupa aproximadamente 3,84 MB por minuto de chamada.

Cada item tem recording.status: not_recorded, recording (captura ou finalização), ready, failed ou expired. Quando pronto, downloadPath aponta para GET /call/{id}/recording. Use o header token no backend: essa rota não aceita credencial na query. Faça proxy autenticado para o player do Atendro, repassando Range para permitir avançar no áudio. O console já faz isso sem expor o token ao navegador.

Depois do prazo, o download responde 410 recording_expired; a limpeza dos arquivos roda de hora em hora, preservando o histórico. Um worker reiniciado durante uma chamada marca o registro como encerrado por worker_restarted e a gravação incompleta como falha. Disco indisponível ou fila de gravação esgotada também gera falha explícita, sem bloquear a chamada.

Os eventos antigos ainda existentes são importados para o histórico pela migração. Não há áudio recuperável de chamadas anteriores à ativação.

Comportamento ao receber uma ligação

Servidores com capabilities.calls_incoming_policy=true aceitam incomingCallMode em POST /instance/call-settings (ou na variante administrativa por ID). A atualização é parcial: alterar apenas o modo preserva o limite, e alterar apenas o limite preserva o modo.

ModoResultado
allowA chamada fica disponível para um atendente aceitar. É o padrão das instâncias existentes.
rejectO servidor recusa a chamada recebida, mesmo com o navegador fechado.
reject_replyO servidor recusa e publica a solicitação de resposta automática para o consumidor.

O modo e callsConfigVersion são congelados na chegada da chamada. A mudança vale para chamadas novas, sem interromper atendimentos em andamento. Cada atualização aceita publica o snapshot completo em call_settings. Para impedir que duas edições concorrentes sobrescrevam uma à outra, envie expectedCallsConfigVersion; divergência retorna 409 settings_version_conflict.

json
{
  "incomingCallMode": "reject_reply",
  "expectedCallsConfigVersion": 3
}

A instância calls não envia mensagens. No Atendro-AI, configure explicitamente o texto e uma conexão de mensagens da mesma empresa e do mesmo número. A recusa e o envio são operações independentes: o histórico precisa mostrar resposta pendente, enviada ou falha, sem afirmar que recusar comprovou o envio.

Os snapshots/eventos de chamada recebida incluem incomingCallMode e callsConfigVersion. Uma recusa confirmada inclui rejectionSource (policy, busy ou manual). autoReplyRequested=true aparece apenas após recusa bem-sucedida por política reject_reply; não envie texto a partir de qualquer evento ended/rejected. Chamadas rejeitadas por ocupação também geram histórico/eventos, sem ocupar uma vaga adicional.

O consumidor deve deduplicar por servidor, instância e chamada, usar a configuração da versão recebida e limitar a frequência de resposta por contato. Eventos repetidos, atrasados e reprocessados não autorizam uma segunda mensagem. Falta de identidade resolvida ou de conexão válida gera falha explícita. Uma chamada não atendida expira localmente: isso não equivale a recusa nem autoriza responder. A recusa explícita pode afetar o toque em outros aparelhos vinculados à conta.

Limites de validação

A bancada validou uma chamada curta de saída com áudio nos dois sentidos, e o responsável confirmou a chamada real pelo painel da VPS em 18/09/2026. Após o ajuste de áudio, repetiu a chamada e confirmou melhora tanto do ruído quanto do eco. Essa confirmação é qualitativa, no ambiente testado. Latência boca-ouvido medida, chamada longa, recebimento e o defeito upstream #25 (recusa por aparelho Web/Desktop do destino) continuam pendentes de homologação. Veja validação e limites.