Skip to main content
As chamadas recebidas permitem que o seu aplicativo saiba, no instante em que o telefone toca, que alguém está ligando para o número da sessão — e que atenda ou recuse essa chamada pela API. Quando você atende, a D-API devolve um WebSocket de mídia idêntico ao das Ligações por Stream: o áudio trafega nos dois sentidos pelo mesmo canal.
Você precisa de uma conexão WhatsApp já conectada (provider whatsmeow) e de uma API Key. Gere a sua chave no painel.

Como funciona

1

Ative o recebimento na conexão

POST /sessions/{sessionId}/receive-calls-settings liga a capacidade. A conexão reinicia.
2

Assine os eventos

Abra o WebSocket /voip/events e fique escutando. A oferta (call.offer) chega por ali.
3

Atenda ou recuse

POST /calls/accept devolve a wsUrl da mídia. POST /calls/reject encerra a oferta.
4

Troque áudio

Abra a wsUrl e siga exatamente o fluxo da página de Ligações por Stream.
O canal de eventos e o canal de mídia são WebSockets diferentes. O de eventos é único, de longa duração e cobre várias sessões; o de mídia é por chamada e nasce a cada accept.

Passo 1 — Ative o recebimento na conexão

O recebimento vem desligado por padrão. Enquanto estiver desligado, a conexão não engaja chamadas entrantes: você continua recebendo o webhook call.offer, mas a mídia não sobe e a chamada não é atendível pelo gateway.
Chamadas recebidas exigem uma conexão não-oficial. A API Oficial (Cloud API) não expõe API de chamadas e não tem o pod de bridge que fala o protocolo de chamada do WhatsApp: receive-calls-settings responde 400 para conexões desse tipo, assim como /calls/accept e /calls/reject.
Esta chamada reinicia a conexão. O valor é gravado no banco e aplicado como variável de ambiente no pod da sessão, o que dispara um restart. Durante o reinício a sessão fica indisponível por alguns segundos: mensagens não são enviadas nem recebidas nesse intervalo. Faça a mudança em janela controlada, não a cada chamada.
Se a atualização da variável de ambiente falhar, o valor continua persistido e passa a valer na próxima subida do pod. A resposta ainda é de sucesso nesse caso.
Para desligar, envie { "receiveCalls": false }. A conexão reinicia de novo.

Passo 2 — Assine os eventos em tempo real

Abra um WebSocket em /voip/events. A autenticação vai na query string, porque o navegador não define cabeçalhos em WebSocket.
string
required
Sua API Key. Aceita chave de usuário ou chave com escopo de sessão — nesse caso, só as sessões dela passam.
string
required
Lista de sessionId separados por vírgula. Valores repetidos e vazios são descartados. Máximo de 20 sessões por conexão.
A URL embute a sua API Key. Abra o socket pelo seu backend, ou entregue ao navegador uma URL gerada sob demanda com uma chave de escopo de sessão. Não registre essa URL em log.
A autorização é tudo ou nada: cada sessão da lista é verificada individualmente (chave válida e sessão pertencente ao dono da chave). Se uma falhar, a conexão inteira é recusada com unauthorized. Um cliente que recebesse um subconjunto silenciosamente acharia que está ouvindo sessões que nunca assinou. Assim que a assinatura é aceita, o servidor envia o primeiro frame:
Se a assinatura for recusada, chega um frame de erro e o socket fecha com o código 1008:

Sem replay de histórico

O socket entrega apenas os eventos que chegarem com ele já conectado. Não há reentrega de backlog, e isso é intencional: uma oferta de chamada vale poucos segundos e não é atendível depois disso. Reentregar o histórico faria o seu app anunciar uma ligação que já acabou — junto com o call.rejected dela.
A consequência prática: quem reconectar no meio de uma oferta perde aquela oferta. Mantenha o socket aberto de forma contínua, com reconexão automática, e trate a queda como perda de chamadas naquele intervalo. Para trilha histórica completa, use os webhooks de chamadas — eles continuam sendo entregues em paralelo.

Formato dos frames

Todos os frames são texto JSON. Este canal é somente leitura: atender e recusar são chamadas HTTP, e o áudio vive no outro WebSocket.
string
required
Um de call.offer, call.accepted, call.ended ou call.rejected.
string
required
Identificador da chamada. É o que você envia em /calls/accept e /calls/reject.
string
JID de quem está ligando, já resolvido para o número de telefone quando possível. Opcional.
string
JID original informado pelo bridge. Pode vir como @lid. Opcional.
number
required
Momento do evento em milissegundos (epoch). Quando o bridge não carimba o evento, vale o horário de chegada no servidor.
string
required
Conexão que recebeu a chamada. Acrescentado pelo servidor, porque um socket cobre várias sessões e o accept exige o sessionId.

Passo 3 — Atenda a chamada

POST /calls/accept com o sessionId e o callId que vieram no call.offer.
string
Identificador da chamada confirmado pelo bridge. Pode diferir do que você enviou — use sempre o valor da resposta daqui em diante.
string
URL do WebSocket de mídia, com callId e token já embutidos. Abra-a exatamente como veio.
string
Token de acesso àquela chamada (a sua API Key). Já está dentro da wsUrl.
A resposta tem o mesmo formato de POST /calls/stream. A partir daqui o áudio, os eventos de ciclo de vida, o mute, o desligar e a gravação funcionam exatamente como descrito em Ligações por Stream — PCM s16le, 16 kHz, mono, frames de 320 amostras. Essa documentação não é repetida aqui.
A wsUrl embute a sua API Key no parâmetro token. Chame /calls/accept pelo seu backend e entregue ao navegador apenas a wsUrl.
Erros possíveis:
Sessão inexistente ou fora do seu escopo.
Conexão da API Oficial (Cloud API), ou o bridge recusou o atendimento — a oferta pode já ter expirado ou sido atendida em outro lugar. Nada é registrado nesse caso: você não recebe uma wsUrl que nunca entregaria áudio.
O stream de voz está indisponível no momento.

Passo 4 — Recuse a chamada

POST /calls/reject precisa de três campos. Além do sessionId e do callId, vai o from: o JID de quem ligou, exatamente como veio no call.offer.
Repasse o from sem transformar. O WhatsApp rejeita contra aquele JID, não contra um número normalizado — se o evento trouxe um @lid, envie o @lid.
O call.rejected correspondente é assíncrono: ele chega depois, pelo socket de eventos e pelo webhook. A resposta 200 significa que o comando foi aceito pelo bridge, não que a outra ponta já parou de tocar.

Exemplo completo

Um cliente Node que assina os eventos, atende a primeira oferta e conecta no WebSocket de mídia. A API Key nunca sai do servidor.
Para recusar em vez de atender, troque a chamada a atender por:

Próximos passos

Ligações por Stream

O WebSocket de mídia em detalhe: formato de áudio, ciclo de vida e gravação.

Webhooks de chamadas

A trilha server-to-server dos mesmos eventos, com histórico.