Guias/Fluxos de chamadas diretas
Documentação

Fluxos de chamadas diretas

Passo a passo para chamada de saida, recebida, atendimento, midia e encerramento.

Este documento mostra os dois fluxos mais comuns de chamada 1:1:

  1. a API origina a chamada para um destinatario;
  2. um cliente externo liga para o numero conectado na API e a API atende.

O objetivo aqui e documentar o caminho que o dashboard precisa seguir: endpoints HTTP, eventos recebidos no WebSocket de eventos e o momento correto de abrir o WebSocket binario de midia para enviar buffers de audio para o backend.

WebSockets usados no fluxo

Existem dois WebSockets diferentes:

  • WebSocket de eventos:
GET /ws/instance/events?event=<EVENTO>&token=<INSTANCE_JWT>
  • WebSocket de midia da chamada:
GET /ws/instance/{instanceName}/calls/{callId}/media?token=<INSTANCE_JWT>

Cada conexao de eventos assina exatamente um evento. Nao existe wildcard. Na pratica, para uma tela de chamadas diretas, o front normalmente abre conexoes para os eventos que deseja observar, por exemplo:

/ws/instance/events?event=call.incoming&token=<INSTANCE_JWT>
/ws/instance/events?event=call.ringing&token=<INSTANCE_JWT>
/ws/instance/events?event=call.ready&token=<INSTANCE_JWT>
/ws/instance/events?event=call.ended&token=<INSTANCE_JWT>
/ws/instance/events?event=call.ended_unconfirmed&token=<INSTANCE_JWT>

Para iniciar o envio de audio local, use call.ready com data.call.status === "ACTIVE" como gatilho principal. A API tambem publica call.active, mas call.ready e o sinal explicito de que o provider confirmou a sessao de midia.

Body dos eventos call.*

Os eventos recebidos no WebSocket de eventos seguem o envelope padrao de webhook. Para chamadas, o campo data contem o envelope especifico da chamada.

Exemplo de call.ready:

{
  "event": "call.ready",
  "instance": {
    "id": 42,
    "name": "test_001",
    "connectionStatus": "online",
    "ownerJid": "[email protected]",
    "externalAttributes": {}
  },
  "data": {
    "event": "call.ready",
    "instance": "test_001",
    "instanceId": 42,
    "call": {
      "id": "01900000-0000-7000-8000-000000000001",
      "providerCallId": "00OUTBOUND123",
      "peer": "5531999999999",
      "direction": "outgoing",
      "status": "ACTIVE",
      "answeredBy": "api",
      "audio": true,
      "video": false
    },
    "data": {},
    "sequence": 4,
    "timestamp": "2026-07-23T20:00:04Z"
  },
  "timestamp": "2026-07-23T20:00:04Z"
}

No dashboard, os campos mais importantes ficam em:

  • event: nome do evento assinado.
  • data.call.id: UUID interno da chamada. Use este valor nos endpoints HTTP e no WebSocket de midia.
  • data.call.providerCallId: ID do provider. Tambem pode ser usado em rotas que aceitam referencia de chamada.
  • data.call.direction: incoming ou outgoing.
  • data.call.status: estado atual da chamada.
  • data.call.peer: numero/JID do outro lado.
  • data.sequence: sequencia monotona por chamada.

answeredBy: "api" indica que a API assumiu a midia da chamada. No fluxo de saida, o sinal pratico de que o destinatario atendeu e o recebimento de call.ready/ACTIVE.

Fluxo 1: API origina a chamada

Neste fluxo, o dashboard ou outro cliente HTTP chama a API para iniciar uma ligacao para um destinatario. Depois que o destinatario atende, o front abre o WebSocket binario de midia e comeca a enviar os buffers de audio para o backend.

1. Iniciar a chamada

Endpoint:

POST /call/{instanceName}
Authorization: Bearer <INSTANCE_JWT>
Content-Type: application/json
Idempotency-Key: crm-call-123

Body:

{
  "target": "5531999999999",
  "video": false,
  "externalId": "crm-call-123",
  "recording": {
    "enabled": false,
    "audio": false,
    "video": false
  }
}

Campos principais:

  • target: obrigatorio. Numero de telefone ou JID direto do WhatsApp.
  • video: false para chamada de audio; true para chamada de video, se habilitada.
  • externalId: opcional. Tambem pode ser preenchido pelo header Idempotency-Key; evita duplicidade em retentativas.
  • recording: opcional. Controla solicitacao de gravacao.

Retorno 201:

{
  "id": "01900000-0000-7000-8000-000000000001",
  "instanceId": 42,
  "instance": "test_001",
  "provider": "meowcaller",
  "providerCallId": "00OUTBOUND123",
  "direction": "outgoing",
  "status": "OUTGOING",
  "peer": "5531999999999",
  "audio": true,
  "video": false,
  "recordingEnabled": false,
  "recordingStatus": "NOT_REQUESTED",
  "externalId": "crm-call-123",
  "startedAt": "2026-07-23T20:00:00Z",
  "durationSeconds": 0,
  "version": 1,
  "stateVersion": 1,
  "createdAt": "2026-07-23T20:00:00Z",
  "updatedAt": "2026-07-23T20:00:00Z"
}

Neste momento a chamada foi criada e enviada ao provider, mas o dashboard ainda nao deve abrir o WebSocket de midia como se a conversa ja estivesse ativa.

2. Acompanhar toque no destinatario

Quando o provider informar que a chamada de saida esta tocando no aparelho do destinatario, a API publica:

call.ringing

Exemplo de body no WebSocket:

{
  "event": "call.ringing",
  "instance": {
    "id": 42,
    "name": "test_001",
    "connectionStatus": "online",
    "ownerJid": "[email protected]",
    "externalAttributes": {}
  },
  "data": {
    "event": "call.ringing",
    "instance": "test_001",
    "instanceId": 42,
    "call": {
      "id": "01900000-0000-7000-8000-000000000001",
      "providerCallId": "00OUTBOUND123",
      "peer": "5531999999999",
      "direction": "outgoing",
      "status": "RINGING",
      "audio": true,
      "video": false
    },
    "data": {
      "reason": "",
      "errorCode": ""
    },
    "sequence": 2,
    "timestamp": "2026-07-23T20:00:01Z"
  },
  "timestamp": "2026-07-23T20:00:01Z"
}

Use este evento apenas para atualizar a UI para "chamando". Ele ainda nao e o ponto de inicio do envio de audio.

3. Destinatario atende

Quando o destinatario atende e o provider confirma que a midia esta pronta, a API publica call.active e, em seguida, call.ready.

O dashboard deve usar:

call.ready

com:

data.call.status === "ACTIVE"

Este e o ponto em que o front sabe que pode abrir o WebSocket de midia e iniciar o envio dos buffers.

4. Abrir WebSocket de midia e enviar audio

Endpoint:

GET /ws/instance/{instanceName}/calls/{callId}/media?token=<INSTANCE_JWT>&audioSend=true&audioReceive=true

Exemplo:

ws://localhost:8084/ws/instance/test_001/calls/01900000-0000-7000-8000-000000000001/media?token=<INSTANCE_JWT>&audioSend=true&audioReceive=true

O callId pode ser o UUID interno (data.call.id) ou o providerCallId, mas o UUID interno e a opcao recomendada no dashboard.

O WebSocket de midia usa frames binarios. Para chamada direta, o protocolo padrao e WMC1.

Header do frame:

OffsetTamanhoDescricao
04Magic WMC1.
41Versao 1.
51Tipo: 1 audio PCM float32, 2 video H.264 Annex-B.
62Reservado.
84Duracao em ms, big-endian.
124Tamanho do payload, big-endian.
16NPayload.

Para audio do dashboard para a API:

  • tipo: 1;
  • payload: PCM float32 little-endian;
  • canal: mono;
  • sample rate: 16 kHz;
  • frame recomendado: cerca de 60 ms.

Exemplo de empacotamento:

function mediaFrame(kind, payload, durationMs = 0) {
  const header = new ArrayBuffer(16);
  const view = new DataView(header);

  view.setUint8(0, 0x57); // W
  view.setUint8(1, 0x4d); // M
  view.setUint8(2, 0x43); // C
  view.setUint8(3, 0x31); // 1
  view.setUint8(4, 1);    // version
  view.setUint8(5, kind); // 1 = audio
  view.setUint32(8, durationMs, false);
  view.setUint32(12, payload.byteLength, false);

  const out = new Uint8Array(16 + payload.byteLength);
  out.set(new Uint8Array(header), 0);
  out.set(new Uint8Array(payload), 16);
  return out;
}

const ws = new WebSocket(
  `ws://localhost:8084/ws/instance/test_001/calls/${callId}/media?token=${token}&audioSend=true&audioReceive=true`
);

ws.binaryType = "arraybuffer";

// float32PcmBuffer = ArrayBuffer com PCM float32 little-endian mono/16 kHz.
ws.send(mediaFrame(1, float32PcmBuffer));

O servidor tambem pode enviar frames de audio remoto no mesmo WebSocket, usando o mesmo envelope binario e kind = 1.

5. Encerrar a chamada de saida

Se a API/dashboard encerrar:

POST /call/{instanceName}/{callId}/hangup
Authorization: Bearer <INSTANCE_JWT>
Content-Type: application/json

Body:

{
  "reason": "dashboard_hangup"
}

Retorno 200:

{
  "call": {
    "id": "01900000-0000-7000-8000-000000000001",
    "instanceId": 42,
    "instance": "test_001",
    "provider": "meowcaller",
    "providerCallId": "00OUTBOUND123",
    "direction": "outgoing",
    "status": "ENDING",
    "peer": "5531999999999",
    "audio": true,
    "video": false,
    "answeredBy": "api",
    "endedBy": "api",
    "endReason": "dashboard_hangup"
  },
  "termination": {
    "requestedAt": "2026-07-23T20:01:00Z",
    "sentAt": "2026-07-23T20:01:00Z",
    "sent": true,
    "confirmed": false,
    "signalAcknowledged": false,
    "remoteTerminationObserved": false,
    "confirmationTimedOut": false,
    "attempts": 1,
    "stanzaId": "ABCDEF123",
    "to": "[email protected]",
    "callCreator": "[email protected]"
  }
}

Depois do hangup, nao feche a UI apenas por receber call.ending. Aguarde um evento terminal:

  • call.ended: encerramento confirmado.
  • call.ended_unconfirmed: terminate enviado, mas sem confirmacao remota dentro do timeout.

Se o destinatario encerrar a chamada, a API publica um evento terminal, normalmente call.ended, com data.call.endedBy indicando o outro lado/provedor conforme o motivo recebido.

Exemplo:

{
  "event": "call.ended",
  "instance": {
    "id": 42,
    "name": "test_001",
    "connectionStatus": "online",
    "ownerJid": "[email protected]",
    "externalAttributes": {}
  },
  "data": {
    "event": "call.ended",
    "instance": "test_001",
    "instanceId": 42,
    "call": {
      "id": "01900000-0000-7000-8000-000000000001",
      "providerCallId": "00OUTBOUND123",
      "peer": "5531999999999",
      "direction": "outgoing",
      "status": "ENDED",
      "answeredBy": "api",
      "endedBy": "peer",
      "endReason": "terminate",
      "audio": true,
      "video": false
    },
    "data": {
      "reason": "terminate",
      "errorCode": ""
    },
    "sequence": 7,
    "timestamp": "2026-07-23T20:01:05Z"
  },
  "timestamp": "2026-07-23T20:01:05Z"
}

Fluxo 2: cliente externo liga para o numero conectado na API

Neste fluxo, o cliente externo nao esta conectado a API. Ele liga pelo WhatsApp para o numero que esta conectado em uma instancia da API. A API recebe a chamada, publica o evento de toque, o dashboard atende via endpoint HTTP e so depois de call.ready abre o WebSocket de midia.

1. Receber evento de chamada tocando

O dashboard deve estar assinando:

GET /ws/instance/events?event=call.incoming&token=<INSTANCE_JWT>

Quando alguem liga para o numero conectado na API, o evento recebido e:

call.incoming

Exemplo de body:

{
  "event": "call.incoming",
  "instance": {
    "id": 42,
    "name": "test_001",
    "connectionStatus": "online",
    "ownerJid": "[email protected]",
    "externalAttributes": {}
  },
  "data": {
    "event": "call.incoming",
    "instance": "test_001",
    "instanceId": 42,
    "call": {
      "id": "01900000-0000-7000-8000-000000000010",
      "providerCallId": "00INBOUND123",
      "peer": "5531999999999",
      "direction": "incoming",
      "status": "RINGING",
      "audio": true,
      "video": false
    },
    "data": {},
    "sequence": 1,
    "timestamp": "2026-07-23T21:00:00Z"
  },
  "timestamp": "2026-07-23T21:00:00Z"
}

Use data.call.id para atender pela API.

2. Atender pela API

Endpoint:

POST /call/{instanceName}/{callId}/answer
Authorization: Bearer <INSTANCE_JWT>

Exemplo:

curl -X POST "$API/call/test_001/01900000-0000-7000-8000-000000000010/answer" \
  -H "Authorization: Bearer $TOKEN"

Retorno 200:

{
  "id": "01900000-0000-7000-8000-000000000010",
  "instanceId": 42,
  "instance": "test_001",
  "provider": "meowcaller",
  "providerCallId": "00INBOUND123",
  "direction": "incoming",
  "status": "CONNECTING",
  "peer": "5531999999999",
  "audio": true,
  "video": false,
  "answeredBy": "api",
  "answerRequestedBy": "api",
  "startedAt": "2026-07-23T21:00:00Z",
  "answeredAt": "2026-07-23T21:00:03Z",
  "durationSeconds": 0,
  "version": 3,
  "stateVersion": 3,
  "createdAt": "2026-07-23T21:00:00Z",
  "updatedAt": "2026-07-23T21:00:03Z"
}

Apos esse endpoint, a API publica eventos intermediarios como:

  • call.answer.requested;
  • call.connecting;
  • call.active;
  • call.ready.

O front deve iniciar o WebSocket de midia apenas no call.ready com data.call.status === "ACTIVE".

3. Chamada atendida e midia pronta

Exemplo de call.ready para chamada recebida:

{
  "event": "call.ready",
  "instance": {
    "id": 42,
    "name": "test_001",
    "connectionStatus": "online",
    "ownerJid": "[email protected]",
    "externalAttributes": {}
  },
  "data": {
    "event": "call.ready",
    "instance": "test_001",
    "instanceId": 42,
    "call": {
      "id": "01900000-0000-7000-8000-000000000010",
      "providerCallId": "00INBOUND123",
      "peer": "5531999999999",
      "direction": "incoming",
      "status": "ACTIVE",
      "answeredBy": "api",
      "audio": true,
      "video": false
    },
    "data": {},
    "sequence": 4,
    "timestamp": "2026-07-23T21:00:04Z"
  },
  "timestamp": "2026-07-23T21:00:04Z"
}

Depois desse evento, abra:

GET /ws/instance/test_001/calls/01900000-0000-7000-8000-000000000010/media?token=<INSTANCE_JWT>&audioSend=true&audioReceive=true

A partir dai, o dashboard envia os envelopes binarios WMC1 com os buffers PCM float32 para o backend, como descrito no fluxo de saida.

4. Encerrar a chamada recebida

Se o dashboard/API encerrar, use o mesmo endpoint:

POST /call/{instanceName}/{callId}/hangup
Authorization: Bearer <INSTANCE_JWT>
Content-Type: application/json

Body:

{
  "reason": "dashboard_hangup"
}

O dashboard deve aguardar:

  • call.ended, quando o encerramento for confirmado;
  • call.ended_unconfirmed, quando a API enviou o terminate, mas nao recebeu confirmacao ate o timeout.

Se o cliente externo encerrar, o dashboard recebe call.ended sem precisar chamar /hangup.

Exemplo de evento quando o cliente externo encerra:

{
  "event": "call.ended",
  "instance": {
    "id": 42,
    "name": "test_001",
    "connectionStatus": "online",
    "ownerJid": "[email protected]",
    "externalAttributes": {}
  },
  "data": {
    "event": "call.ended",
    "instance": "test_001",
    "instanceId": 42,
    "call": {
      "id": "01900000-0000-7000-8000-000000000010",
      "providerCallId": "00INBOUND123",
      "peer": "5531999999999",
      "direction": "incoming",
      "status": "ENDED",
      "answeredBy": "api",
      "endedBy": "peer",
      "endReason": "terminate",
      "audio": true,
      "video": false
    },
    "data": {
      "reason": "terminate",
      "errorCode": ""
    },
    "sequence": 6,
    "timestamp": "2026-07-23T21:05:00Z"
  },
  "timestamp": "2026-07-23T21:05:00Z"
}

Resumo de eventos por fluxo

MomentoFluxo API -> destinatarioFluxo cliente externo -> API
Chamada criadacall.outgoing-
Tocandocall.ringingcall.incoming com status=RINGING
Atendimento solicitado pela API-call.answer.requested
Conectandocall.connecting, se emitido pelo providercall.connecting
Midia prontacall.ready com status=ACTIVEcall.ready com status=ACTIVE
API desligandocall.hangup.requested, call.ending, call.terminate.sentcall.hangup.requested, call.ending, call.terminate.sent
Encerrado confirmadocall.endedcall.ended
Encerrado sem confirmacaocall.ended_unconfirmedcall.ended_unconfirmed

Reconciliacao apos reconnect

Se o dashboard reconectar ou perder algum evento, consulte:

GET /call/{instanceName}/{callId}
Authorization: Bearer <INSTANCE_JWT>

Inicie ou retome a midia apenas se o retorno estiver com:

status === "ACTIVE"

Para auditoria da sequencia recebida:

GET /call/{instanceName}/{callId}/events
Authorization: Bearer <INSTANCE_JWT>