Bemmelhor Telecom · Developers

API Docs

Referência completa para integrar ligações, gravações, Telefonia & URA, webhooks em tempo real e o softphone WebRTC no seu CRM ou portal.

Começar

Introdução

A API de integrações da Bemmelhor permite consultar ligações, obter gravações e transcrições, disparar click-to-call, gerenciar Telefonia & URA, receber eventos via webhook e incorporar o softphone Web.

Base URL
https://service.bemmelhor.com.br/api/integrations/v1
Formato
JSON · HTTPS · UTF-8
Autenticação
API Key (Bearer ou x-api-key)
Escopos
Cada endpoint exige o escopo correspondente na chave

Começar

Autenticação

Crie uma chave de API no painel (Organização → Segurança e API). A chave é exibida apenas uma vez. Envie-a em todas as requisições à API de integrações.

Headers aceitos

  • Authorization: Bearer <sua_chave>
  • x-api-key: <sua_chave>

Escopos da API de integração

  • calls:list

    Listar ligações

  • calls:get

    Ver ligação

  • recording:get

    Ver gravação

  • transcription:get

    Ver transcrição

  • summary:get

    Ver resumo

  • click_to_call

    Click to call

  • calls:transfer

    Transferir chamada

  • extensions:status

    Ramais e Status

Escopos Telefonia & URA

CRUD de ramais, filas, áudios, URAs e DIDs via Bridge. Disponível somente para organizações com integração partner / Telefonia & WhatsApp (setup Bridge concluído). Para status em tempo real dos ramais em qualquer org com Asterisk, use o escopo extensions:status (API de integração).

  • telephony_bridge:status:read

    Telefonia, status

  • telephony_bridge:extensions:read

    Ramais, leitura

  • telephony_bridge:extensions:write

    Ramais, escrita

  • telephony_bridge:queues:read

    Filas, leitura

  • telephony_bridge:queues:write

    Filas, escrita

  • telephony_bridge:audios:read

    Áudios, leitura

  • telephony_bridge:audios:write

    Áudios, escrita

  • telephony_bridge:ivrs:read

    URAs, leitura

  • telephony_bridge:ivrs:write

    URAs, escrita

  • telephony_bridge:dids:read

    DIDs, leitura

  • telephony_bridge:dids:write

    DIDs, escrita

curl "https://service.bemmelhor.com.br/api/integrations/v1/calls?page=1&limit=20" \
  -H "x-api-key: SUA_CHAVE_DE_API"

API de integração

Endpoints

Base URL: https://service.bemmelhor.com.br/api/integrations/v1

GET/callscalls:list

Listar ligações

Retorna lista paginada de ligações da organização.

Parâmetros

NomeLocalTipoDescrição
pagequerynumberPágina (default: 1)
limitquerynumberItens por página (default: 50, máx: 100)
typequerystringENTRANTE ou SAINTE
startDatequerystringData inicial (ISO)
endDatequerystringData final (ISO)
originquerystringRamal de origem
destinyquerystringRamal de destino
sectorquerystringSetor
dispositionquerystringANSWER, NO ANSWER, BUSY, FAILED
queuequerystringNome da fila

Query: marque e preencha para incluir no exemplo

Exemplo de requisição

# Listar ligações
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/calls?page=1&limit=50" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/calls/:callIdcalls:get

Ver ligação

Retorna os detalhes de uma ligação pelo ID.

Parâmetros

NomeLocalTipoDescrição
callIdobrigatóriopathstringID da ligação (ou external-{id})

Exemplo de requisição

# Ver ligação
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/calls/abc123-external-456" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/recordings/:linkedIdrecording:get

Ver gravação

Retorna o stream de áudio da gravação. Use o linkedId da ligação (campo LinkedID do CDR/webhook). Se não encontrar, tente uniqueId e, por último, o callId.

Parâmetros

NomeLocalTipoDescrição
linkedIdobrigatóriopathstringIdentificador da ligação. Prefira o linkedId (LinkedID). Se a gravação não for encontrada, use uniqueId e, por redundância, o callId.

Exemplo de requisição

# Ver gravação
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/recordings/1735123456.123" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/calls/:callId/transcriptiontranscription:get

Ver transcrição

Retorna a transcrição da ligação. Prefira o linkedId no path; se não encontrar, use uniqueId ou callId. Se não existir e a integração estiver habilitada, gera e retorna.

Parâmetros

NomeLocalTipoDescrição
callIdobrigatóriopathstringIdentificador da ligação. Prefira linkedId (LinkedID do CDR). Por redundância, se não encontrar, use uniqueId ou o callId interno.

Exemplo de requisição

# Ver transcrição
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/calls/1735123456.123/transcription" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/calls/:callId/summarysummary:get

Ver resumo

Retorna o resumo da ligação. Prefira o linkedId no path; se não encontrar, use uniqueId ou callId. Se não existir e a integração estiver habilitada, gera transcrição e resumo.

Parâmetros

NomeLocalTipoDescrição
callIdobrigatóriopathstringIdentificador da ligação. Prefira linkedId (LinkedID do CDR). Por redundância, se não encontrar, use uniqueId ou o callId interno.

Exemplo de requisição

# Ver resumo
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/calls/1735123456.123/summary" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/click-to-callclick_to_call

Click to call

Inicia uma ligação entre o ramal e o número informado.

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatóriobodystringNúmero do ramal
phoneNumberobrigatóriobodystringNúmero de telefone de destino

Corpo de exemplo

{
  "extensionNumber": "100",
  "phoneNumber": "11999999999"
}

Exemplo de requisição

# Click to call
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/click-to-call" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/calls/supervised-transfercalls:transfer

Transferência assistida

Com o ramal em ligação, inicia transferência assistida. Responde 202 imediatamente (destino tocando); o agente permanece com o cliente até conclusão, cancelamento, falha ou timeout. Emite TransferStarted e os eventos finais via webhook e /ws/telephony-events. Consulte o status em GET /calls/supervised-transfer/:transferId.

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatóriobodystringRamal origem (em chamada)
targetExtensionobrigatóriobodystringRamal destino

Corpo de exemplo

{
  "extensionNumber": "6073",
  "targetExtension": "6010"
}

Respostas

202Aceito

Transferência iniciada. O destino está tocando; acompanhe o resultado pelo status, webhook ou WebSocket.

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "ringing",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "message": "Aguardando o ramal destino atender…"
}
400Requisição inválida

Body incompleto, ramais iguais ou ligação inválida. Valores possíveis de error: dados inválidos (campos vazios); O ramal origem não pode ser o mesmo que o ramal destino; O ramal não está em ligação no momento; Não foi possível identificar o canal do cliente para a transferência. Verifique se a ligação ainda está ativa.

{
  "error": "O ramal não está em ligação no momento"
}
401Não autenticado

API key ausente, inválida ou expirada.

{
  "error": "Credenciais inválidas ou expiradas"
}
403Sem permissão

A chave não possui o escopo calls:transfer (ou a organização está com restrição).

{
  "error": "Chave de API sem permissão para este recurso"
}
404Não encontrado

Valores possíveis de error: Ramal origem não encontrado nesta organização; Ramal origem não encontrado ou não está dentro dos ranges permitidos; Ramal destino não encontrado ou não está dentro dos ranges permitidos; Organização não encontrada; Configuração Asterisk não encontrada.

{
  "error": "Ramal origem não encontrado nesta organização"
}
409Conflito

Já existe uma transferência assistida em ringing neste ramal origem. Cancele ou aguarde o término antes de iniciar outra.

{
  "error": "Já existe uma transferência assistida em andamento neste ramal"
}
500Erro interno

Falha inesperada ao iniciar a transferência.

{
  "error": "Erro ao iniciar transferência assistida"
}

Exemplo de requisição

# Transferência assistida
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/calls/supervised-transfer" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/calls/supervised-transfer/cancelcalls:transfer

Cancelar transferência assistida

Solicita o cancelamento da consulta ao destino. A ligação permanece com o agente. Se a transferência ainda estiver ringing, retorna 200 e emite TransferCancelled em seguida. Se já tiver finalizado, também retorna 200 informando o status final.

Parâmetros

NomeLocalTipoDescrição
transferIdobrigatóriobodystringUUID retornado ao iniciar a transferência
extensionNumberbodystringRamal origem (opcional). Se enviado, deve coincidir com o ramal que iniciou a transferência; caso contrário retorna 403.

Corpo de exemplo

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "extensionNumber": "6073"
}

Respostas

200Cancelamento solicitado

A consulta ainda estava ringing. O cancelamento foi aceito; o status no payload pode continuar ringing até o job aplicar o cancelamento.

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "ringing",
  "message": "Cancelamento solicitado"
}
200Já finalizada

A transferência já tinha saído de ringing (completed, failed, cancelled ou timeout). Nenhum cancelamento extra é feito.

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "completed",
  "message": "Transferência já finalizou com status completed"
}
400Requisição inválida

transferId ausente ou que não é UUID. error típico: transferId inválido.

{
  "error": "transferId inválido"
}
401Não autenticado

API key ausente, inválida ou expirada.

{
  "error": "Credenciais inválidas ou expiradas"
}
403Sem permissão

Chave sem o escopo calls:transfer, ou extensionNumber informado não é o ramal que iniciou a transferência.

{
  "error": "Transferência não pertence a este ramal"
}
404Não encontrado

transferId desconhecido, de outra organização ou expirado (TTL de 5 minutos após o fim).

{
  "error": "Transferência assistida não encontrada"
}
500Erro interno

Falha inesperada ao cancelar a transferência.

{
  "error": "Erro ao cancelar transferência assistida"
}

Exemplo de requisição

# Cancelar transferência assistida
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/calls/supervised-transfer/cancel" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/calls/supervised-transfer/:transferIdcalls:transfer

Status da transferência assistida

Consulta o andamento da transferência. status possível: ringing, completed, failed, cancelled ou timeout. Em failed/timeout o campo error pode vir preenchido; em completed, transferMethod indica o método AMI usado.

Parâmetros

NomeLocalTipoDescrição
transferIdobrigatóriopathstringUUID da transferência

Respostas

200Em andamento

Destino ainda tocando (status: ringing).

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "ringing",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "message": "Aguardando o ramal destino atender…"
}
200Concluída

Destino atendeu e o cliente foi conectado (status: completed).

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "completed",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "transferMethod": "bridge_customer_target",
  "message": "Transferência assistida concluída"
}
200Falha

Destino recusou, erro AMI ou cliente desligou (status: failed). error traz o motivo.

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "failed",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "error": "Falha na transferência assistida",
  "message": "Falha na transferência assistida"
}
200Cancelada

O agente cancelou a consulta (status: cancelled).

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "cancelled",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "message": "Transferência assistida cancelada"
}
200Timeout

O destino não atendeu a tempo (~25s). A ligação permanece com o agente (status: timeout).

{
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "timeout",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "error": "O ramal destino não atendeu. A ligação permanece com você.",
  "message": "O ramal destino não atendeu. A ligação permanece com você."
}
400Requisição inválida

transferId ausente no path.

{
  "error": "transferId é obrigatório"
}
401Não autenticado

API key ausente, inválida ou expirada.

{
  "error": "Credenciais inválidas ou expiradas"
}
403Sem permissão

A chave não possui o escopo calls:transfer.

{
  "error": "Chave de API sem permissão para este recurso"
}
404Não encontrado

transferId desconhecido, de outra organização ou expirado.

{
  "error": "Transferência assistida não encontrada"
}
500Erro interno

Falha inesperada ao consultar o status.

{
  "error": "Erro ao consultar status da transferência"
}

Exemplo de requisição

# Status da transferência assistida
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/calls/supervised-transfer/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/extensionsextensions:status

Ramais e Status

Lista ramais com status de registro (online/offline) e de chamada em tempo real (idle, ringing, in_call, dialing). Funciona com chave SK- (scope) ou token partner PT-. Não exige Telefonia & WhatsApp, apenas Asterisk configurado na organização.

Exemplo de requisição

# Ramais e Status
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/extensions" \
  -H "x-api-key: SUA_CHAVE_DE_API"

Telefonia & URA

API Telefonia & URA

CRUD de ramais, filas, áudios, URAs e DIDs via Bridge. Disponível somente para organizações com integração partner / Telefonia & WhatsApp (setup Bridge concluído). Cada endpoint exige o escopo correspondente. Consulta de status em tempo real dos ramais (sem Bridge) está em GET /extensions na API de integração.

Status

GET/telephony-bridge/statustelephony_bridge:status:read

Status do tenant

Retorna visão geral do tenant Bridge (ramais, filas, áudios, URAs e DIDs).

Exemplo de requisição

# Status do tenant
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/status" \
  -H "x-api-key: SUA_CHAVE_DE_API"

Ramais

GET/telephony-bridge/extensionstelephony_bridge:extensions:read

Listar ramais

Lista ramais configurados no tenant.

Parâmetros

NomeLocalTipoDescrição
statusquerystringFiltrar por online ou offline

Query: marque e preencha para incluir no exemplo

Exemplo de requisição

# Listar ramais
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/extensions/:exttelephony_bridge:extensions:read

Obter ramal

Retorna detalhes de um ramal.

Parâmetros

NomeLocalTipoDescrição
extobrigatóriopathstringNúmero do ramal

Exemplo de requisição

# Obter ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions/1001" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/extensionstelephony_bridge:extensions:write

Criar ramal

Cria um novo ramal no tenant.

Parâmetros

NomeLocalTipoDescrição
extensionobrigatóriobodystringNúmero do ramal (3 a 8 dígitos)
display_nameobrigatóriobodystringNome de exibição
transportbodystringwss ou udp
default_queuebodystringFila padrão (opcional)
secretbodystringSenha SIP ou "auto"

Corpo de exemplo

{
  "extension": "1001",
  "display_name": "Atendimento",
  "transport": "wss",
  "secret": "auto"
}

Exemplo de requisição

# Criar ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions" \
  -H "x-api-key: SUA_CHAVE_DE_API"
PATCH/telephony-bridge/extensions/:exttelephony_bridge:extensions:write

Atualizar ramal

Atualiza campos de um ramal existente.

Parâmetros

NomeLocalTipoDescrição
extobrigatóriopathstringNúmero do ramal
display_namebodystringNome de exibição

Corpo de exemplo

{
  "display_name": "Suporte N1"
}

Exemplo de requisição

# Atualizar ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X PATCH "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions/1001" \
  -H "x-api-key: SUA_CHAVE_DE_API"
DELETE/telephony-bridge/extensions/:exttelephony_bridge:extensions:write

Excluir ramal

Remove um ramal do tenant.

Parâmetros

NomeLocalTipoDescrição
extobrigatóriopathstringNúmero do ramal

Exemplo de requisição

# Excluir ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X DELETE "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions/1001" \
  -H "x-api-key: SUA_CHAVE_DE_API"

Filas

GET/telephony-bridge/queuestelephony_bridge:queues:read

Listar filas

Lista filas de atendimento.

Exemplo de requisição

# Listar filas
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/queues/:nametelephony_bridge:queues:read

Obter fila

Retorna detalhes de uma fila.

Parâmetros

NomeLocalTipoDescrição
nameobrigatóriopathstringNome da fila

Exemplo de requisição

# Obter fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues/suporte" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/queuestelephony_bridge:queues:write

Criar fila

Cria uma nova fila.

Corpo de exemplo

{
  "name": "suporte",
  "display_name": "Suporte",
  "strategy": "ringall"
}

Exemplo de requisição

# Criar fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues" \
  -H "x-api-key: SUA_CHAVE_DE_API" \
  -H "Content-Type: application/json" \
  -d '{"name":"suporte","display_name":"Suporte","strategy":"ringall"}'
PATCH/telephony-bridge/queues/:nametelephony_bridge:queues:write

Atualizar fila

Atualiza configuração de uma fila.

Parâmetros

NomeLocalTipoDescrição
nameobrigatóriopathstringNome da fila

Corpo de exemplo

{
  "display_name": "Suporte comercial"
}

Exemplo de requisição

# Atualizar fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X PATCH "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues/suporte" \
  -H "x-api-key: SUA_CHAVE_DE_API" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"Suporte comercial"}'
DELETE/telephony-bridge/queues/:nametelephony_bridge:queues:write

Excluir fila

Remove uma fila.

Parâmetros

NomeLocalTipoDescrição
nameobrigatóriopathstringNome da fila

Exemplo de requisição

# Excluir fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X DELETE "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues/suporte" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/queues/:name/memberstelephony_bridge:queues:write

Membros da fila

Adiciona ou remove ramais de uma fila.

Parâmetros

NomeLocalTipoDescrição
nameobrigatóriopathstringNome da fila
addbodystring[]Ramais a adicionar
removebodystring[]Ramais a remover

Corpo de exemplo

{
  "add": [
    "1001"
  ],
  "remove": [
    "1002"
  ]
}

Exemplo de requisição

# Membros da fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues/suporte/members" \
  -H "x-api-key: SUA_CHAVE_DE_API"

Áudios

GET/telephony-bridge/audiostelephony_bridge:audios:read

Listar áudios

Lista áudios (TTS e upload) do tenant.

Exemplo de requisição

# Listar áudios
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/audios/voicestelephony_bridge:audios:read

Vozes TTS

Lista vozes disponíveis para síntese de áudio.

Exemplo de requisição

# Vozes TTS
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/voices" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/audios/:audioIdtelephony_bridge:audios:read

Obter áudio

Retorna metadados de um áudio.

Parâmetros

NomeLocalTipoDescrição
audioIdobrigatóriopathstringIdentificador do áudio

Exemplo de requisição

# Obter áudio
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/boas_vindas" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/audios/:audioId/rawtelephony_bridge:audios:read

Download do áudio

Retorna o arquivo de áudio (mp3 ou slin).

Parâmetros

NomeLocalTipoDescrição
audioIdobrigatóriopathstringIdentificador do áudio
formatquerystringmp3 (padrão) ou slin

Query: marque e preencha para incluir no exemplo

Exemplo de requisição

# Download do áudio
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/boas_vindas/raw" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/audiostelephony_bridge:audios:write

Criar áudio

Cria áudio por TTS (JSON) ou upload (multipart/form-data com file, audio_id e description). MP3/WAV, máx. 10 MB.

Parâmetros

NomeLocalTipoDescrição
audio_idobrigatóriobodystringSlug do áudio (upload ou TTS)
sourcebodystringtts para síntese
textbodystringTexto para TTS
voicebodystringVoz TTS

Corpo de exemplo

{
  "audio_id": "boas_vindas",
  "source": "tts",
  "text": "Bem-vindo à nossa central.",
  "voice": "pt-br-female"
}

Exemplo de requisição

# Criar áudio
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios" \
  -H "x-api-key: SUA_CHAVE_DE_API"
PATCH/telephony-bridge/audios/:audioIdtelephony_bridge:audios:write

Atualizar metadados do áudio

Atualiza descrição ou metadados sem substituir o conteúdo.

Parâmetros

NomeLocalTipoDescrição
audioIdobrigatóriopathstringIdentificador do áudio
descriptionbodystringNova descrição

Corpo de exemplo

{
  "description": "Saudação inicial"
}

Exemplo de requisição

# Atualizar metadados do áudio
# Parâmetros de path/query já preenchidos com exemplos.
curl -X PATCH "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/boas_vindas" \
  -H "x-api-key: SUA_CHAVE_DE_API"
PUT/telephony-bridge/audios/:audioIdtelephony_bridge:audios:write

Substituir áudio

Substitui conteúdo por novo TTS (JSON) ou upload (multipart/form-data).

Parâmetros

NomeLocalTipoDescrição
audioIdobrigatóriopathstringIdentificador do áudio

Corpo de exemplo

{
  "source": "tts",
  "text": "Texto atualizado.",
  "voice": "pt-br-female"
}

Exemplo de requisição

# Substituir áudio
# Parâmetros de path/query já preenchidos com exemplos.
curl -X PUT "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/boas_vindas" \
  -H "x-api-key: SUA_CHAVE_DE_API" \
  -H "Content-Type: application/json" \
  -d '{"source":"tts","text":"Texto atualizado.","voice":"pt-br-female"}'
DELETE/telephony-bridge/audios/:audioIdtelephony_bridge:audios:write

Excluir áudio

Remove um áudio do tenant.

Parâmetros

NomeLocalTipoDescrição
audioIdobrigatóriopathstringIdentificador do áudio

Exemplo de requisição

# Excluir áudio
# Parâmetros de path/query já preenchidos com exemplos.
curl -X DELETE "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/boas_vindas" \
  -H "x-api-key: SUA_CHAVE_DE_API"

URAs

GET/telephony-bridge/ivrstelephony_bridge:ivrs:read

Listar URAs

Lista URAs configuradas.

Exemplo de requisição

# Listar URAs
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/ivrs/:ivrIdtelephony_bridge:ivrs:read

Obter URA

Retorna configuração completa de uma URA.

Parâmetros

NomeLocalTipoDescrição
ivrIdobrigatóriopathstringIdentificador da URA

Exemplo de requisição

# Obter URA
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs/principal" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/ivrs/:ivrId/didstelephony_bridge:ivrs:read

DIDs da URA

Lista números (DIDs) vinculados à URA.

Parâmetros

NomeLocalTipoDescrição
ivrIdobrigatóriopathstringIdentificador da URA

Exemplo de requisição

# DIDs da URA
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs/principal/dids" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/ivrstelephony_bridge:ivrs:write

Criar URA

Cria uma nova URA de atendimento.

Parâmetros

NomeLocalTipoDescrição
ivr_idobrigatóriobodystringIdentificador da URA
display_nameobrigatóriobodystringNome de exibição

Corpo de exemplo

{
  "ivr_id": "principal",
  "display_name": "URA Principal",
  "greeting_audio_id": "boas_vindas",
  "options": [
    {
      "digit": "1",
      "action": "queue",
      "target": "suporte"
    }
  ]
}

Exemplo de requisição

# Criar URA
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs" \
  -H "x-api-key: SUA_CHAVE_DE_API"
PATCH/telephony-bridge/ivrs/:ivrIdtelephony_bridge:ivrs:write

Atualizar URA

Atualiza campos de uma URA existente.

Parâmetros

NomeLocalTipoDescrição
ivrIdobrigatóriopathstringIdentificador da URA

Corpo de exemplo

{
  "display_name": "URA Atualizada",
  "menu_audio_id": "menu_opcoes"
}

Exemplo de requisição

# Atualizar URA
# Parâmetros de path/query já preenchidos com exemplos.
curl -X PATCH "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs/principal" \
  -H "x-api-key: SUA_CHAVE_DE_API" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"URA Atualizada","menu_audio_id":"menu_opcoes"}'
DELETE/telephony-bridge/ivrs/:ivrIdtelephony_bridge:ivrs:write

Excluir URA

Remove uma URA.

Parâmetros

NomeLocalTipoDescrição
ivrIdobrigatóriopathstringIdentificador da URA

Exemplo de requisição

# Excluir URA
# Parâmetros de path/query já preenchidos com exemplos.
curl -X DELETE "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs/principal" \
  -H "x-api-key: SUA_CHAVE_DE_API"

DIDs

GET/telephony-bridge/didstelephony_bridge:dids:read

Listar DIDs

Lista números de entrada (DIDs).

Exemplo de requisição

# Listar DIDs
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/dids" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/telephony-bridge/dids/:numbertelephony_bridge:dids:read

Obter DID

Retorna detalhes de um número.

Parâmetros

NomeLocalTipoDescrição
numberobrigatóriopathstringNúmero (somente dígitos)

Exemplo de requisição

# Obter DID
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/dids/5514981927913" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/didstelephony_bridge:dids:write

Criar DID

Associa um número a uma URA.

Parâmetros

NomeLocalTipoDescrição
numberobrigatóriobodystringNúmero (10 a 15 dígitos)
ivr_idbodystringURA de destino (null para nenhuma)

Corpo de exemplo

{
  "number": "5514981927913",
  "ivr_id": "principal"
}

Exemplo de requisição

# Criar DID
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/dids" \
  -H "x-api-key: SUA_CHAVE_DE_API"
PATCH/telephony-bridge/dids/:numbertelephony_bridge:dids:write

Atualizar DID

Altera a URA vinculada ao número.

Parâmetros

NomeLocalTipoDescrição
numberobrigatóriopathstringNúmero
ivr_idobrigatóriobodystringNova URA ou null

Corpo de exemplo

{
  "ivr_id": "principal"
}

Exemplo de requisição

# Atualizar DID
# Parâmetros de path/query já preenchidos com exemplos.
curl -X PATCH "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/dids/5514981927913" \
  -H "x-api-key: SUA_CHAVE_DE_API"
DELETE/telephony-bridge/dids/:numbertelephony_bridge:dids:write

Excluir DID

Remove um número do tenant.

Parâmetros

NomeLocalTipoDescrição
numberobrigatóriopathstringNúmero

Exemplo de requisição

# Excluir DID
# Parâmetros de path/query já preenchidos com exemplos.
curl -X DELETE "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/dids/5514981927913" \
  -H "x-api-key: SUA_CHAVE_DE_API"

Webhooks

Eventos em tempo real

Configure um endpoint HTTPS no painel (Segurança → Webhooks) para receber o ciclo de vida completo das chamadas. Apenas os 6 eventos abaixo devem ser considerados; qualquer outro evento da API deve ser ignorado.

Entrega e segurança

Método

POST

Content-Type

application/json

Assinatura

HMAC-SHA256

Headers de segurança

  • X-Webhook-Id: Identificador único do webhook configurado.
  • X-Webhook-Timestamp: Timestamp Unix em milissegundos usado na assinatura.
  • X-Webhook-Signature: Assinatura HMAC-SHA256 no formato v1=hash.

Payload assinado: webhookId.timestamp.bodyJson

Verificar assinatura

Valide que a requisição veio da Bemmelhor recriando a assinatura com o secret do webhook.

const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const webhookId = req.headers['x-webhook-id'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const receivedSignature = req.headers['x-webhook-signature'];
  const bodyJson = JSON.stringify(req.body);
  const payload = `${webhookId}.${timestamp}.${bodyJson}`;
  const expectedSignature = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  );
}

app.post('/webhook', (req, res) => {
  if (!verifyWebhookSignature(req, 'SEU_SECRET_AQUI')) {
    return res.status(401).json({ error: 'Assinatura inválida' });
  }
  res.status(200).send('OK');
});
1

Início de Discagem (Dial)

Representa o momento em que uma tentativa de chamada é iniciada. Aparece em ligações entrantes e sainte. Use para registrar tentativa de chamada, identificar quem liga e o destino.

{
  "Event": "Dial",
  "SubEvent": "Begin",
  "Channel": "SIP/9001-000006da",
  "Destination": "SIP/bina-000006db",
  "CallerIDNum": "9001",
  "UniqueID": "1773334415.1773",
  "DestUniqueID": "1773334415.1774",
  "Dialstring": "bina/5532984184189"
}

Campos importantes

  • CallerIDNum: Quem está ligando (ramal ou número externo).
  • Destination: Canal de destino da chamada.
  • Dialstring: Número discado ou ramal de destino.
  • correlation_id / ToMsisdn / FromDid: Em ligações WhatsApp via Bridge, quando correlacionadas: correlation_id é o mesmo do WhatsAppCallOriginated; ToMsisdn é o destino e FromDid a origem.
  • UniqueID / DestUniqueID: Identificadores únicos para correlacionar eventos.
2

Mudança de Estado (Newstate)

Indica alterações no estado de um canal durante a chamada. ChannelStateDesc: Ringing = ramal tocando, Ring = telefone tocando, Up = chamada atendida. Aparece em entrantes e sainte.

{
  "Event": "Newstate",
  "ChannelStateDesc": "Up",
  "CallerIDNum": "9001",
  "ConnectedLineNum": "32984184189"
}

Campos importantes

  • ChannelStateDesc: Ringing = ramal tocando, Ring = telefone tocando, Up = chamada atendida.
  • CallerIDNum: Número de quem está chamando.
  • ConnectedLineNum: Número da linha conectada (quem atendeu).
3

Chamada Conectada (Bridge Link)

Indica que duas partes da chamada foram conectadas e a conversa começou. Use para iniciar contagem de tempo de conversa e considerar a chamada efetivamente conectada.

{
  "Event": "Bridge",
  "Bridgestate": "Link",
  "Channel1": "SIP/9001-000006f3",
  "Channel2": "SIP/bina-000006f4",
  "Uniqueid1": "1773335384.1802",
  "Uniqueid2": "1773335384.1803",
  "CallerID1": "9001",
  "CallerID2": "032984184189"
}

Campos importantes

  • Channel1 / Channel2: Os dois canais conectados.
  • CallerID1 / CallerID2: Identificação de quem atendeu em cada lado.
  • Uniqueid1 / Uniqueid2: IDs para correlacionar com outros eventos.
4

Chamada Desconectada (Bridge Unlink)

Indica que a conexão entre os canais foi interrompida. A ligação deixa de estar conectada. Use para calcular duração da conversa (BridgeLink → BridgeUnlink).

{
  "Event": "Bridge",
  "Bridgestate": "Unlink",
  "Channel1": "SIP/9001-000006fb",
  "Channel2": "SIP/bina-000006fc"
}

Campos importantes

  • Channel1 / Channel2: Os canais que foram desconectados.
  • Bridgestate: Sempre 'Unlink' neste evento.
5

Fim de Chamada (Hangup)

Indica o encerramento definitivo de um canal da chamada. Pode haver dois eventos Hangup para uma única chamada (um por canal). Cause indica o motivo do desligamento.

{
  "Event": "Hangup",
  "Channel": "SIP/9001-00000709",
  "Uniqueid": "1773336018.1824",
  "CallerIDNum": "9001",
  "ConnectedLineNum": "32984184189",
  "Cause": "16",
  "Cause-txt": "Normal Clearing"
}

Campos importantes

  • Cause: Código do motivo (16 = Normal Clearing, etc.).
  • Cause-txt: Descrição textual do motivo.
  • CallerIDNum / ConnectedLineNum: Identificam quem desligou.
6

Registro de Chamada (CDR)

Disparado somente ao final da chamada, com resumo consolidado. Contém duração, origem, destino, billsec (tempo tarifado), disposition e demais dados do registro. Use para faturamento, relatórios e integrações que precisam de um único evento com resumo da chamada.

{
  "Event": "Cdr",
  "Privilege": "cdr,all",
  "AccountCode": "",
  "Source": "6010",
  "Destination": "0311999887766",
  "DestinationContext": "from-internal",
  "CallerID": "\"6010\" <6010>",
  "Channel": "SIP/6010-00000090",
  "DestinationChannel": "SIP/trunk-00000091",
  "LastApplication": "Dial",
  "LastData": "SIP/trunk/0311999887766,120,TtrM(atenddial^6010)",
  "StartTime": "2026-03-16 15:45:29",
  "AnswerTime": "2026-03-16 15:45:32",
  "EndTime": "2026-03-16 15:45:40",
  "Duration": "11",
  "BillableSeconds": "8",
  "Disposition": "ANSWERED",
  "AMAFlags": "DOCUMENTATION",
  "UniqueID": "1739123456.100",
  "LinkedID": "1739123456.100",
  "UserField": "6010",
  "Queue": "",
  "CallType": "OUTCOMING",
  "DstOperator": "11",
  "DstPhoneNumber": "999887766",
  "DstDDD": "",
  "DstType": "MOVEL",
  "DstDistance": "LDN",
  "ItemCampaignId": ""
}

Campos importantes

  • Source / Destination: Origem e destino da chamada.
  • Duration / BillableSeconds: Duração total e tempo tarifado em segundos.
  • Disposition: Resultado: ANSWERED, NO ANSWER, BUSY, FAILED, etc.
  • StartTime / AnswerTime / EndTime: Timestamps de início, atendimento e fim.
  • LinkedID: Identificador da ligação a usar em gravação, transcrição e resumo. Prefira LinkedID; se não encontrar, use UniqueID ou o callId.
  • UniqueID: Identificador da perna da chamada. Use como fallback se o LinkedID não localizar a gravação/transcrição/resumo.
7

Solicitação de permissão WhatsApp (WhatsAppCallPermissionRequested)

Enviado quando o cliente recebe no WhatsApp o pedido para autorizar ligações oficiais. Usa a mesma assinatura HMAC dos demais webhooks. Serve para mapear organização, DID de origem e número de destino.

{
  "Event": "WhatsAppCallPermissionRequested",
  "organizationId": "58126d91-27db-4195-bf71-d2c72041976b",
  "delivered_via": "webhook",
  "from_did": "5514981927913",
  "to_msisdn": "5514982151280",
  "status": "pending",
  "source": "softphone_embed",
  "gupshup_message_id": "43907c0d-a78d-49ea-8271-248881e9f523",
  "requested_at": "2026-07-30T18:51:19Z",
  "permission_type": "",
  "tenant_id": "cliente_bemmelhor",
  "body_text": ""
}

Campos importantes

  • organizationId: ID da organização que originou a solicitação.
  • from_did: DID ou WhatsApp de origem usado na solicitação.
  • to_msisdn: Número do cliente que recebeu o pedido de aprovação.
  • status: Status da permissão no momento do envio, em geral pending.
  • source: Origem do pedido: softphone, softphone_embed, partner_api ou manual_api.
  • delivered_via: Sempre webhook. Indica que a notificação chegou pelo webhook assinado.
  • gupshup_message_id: ID da mensagem na provedora, quando disponível.
8

Ligação WhatsApp originada (WhatsAppCallOriginated)

Enviado quando a Bridge aceita originar a ligação WhatsApp. Este é o evento confiável para obter o telefone de destino (to_msisdn), porque o Dial AMI da Bridge só mostra o ramal do agente.

{
  "Event": "WhatsAppCallOriginated",
  "organizationId": "58126d91-27db-4195-bf71-d2c72041976b",
  "delivered_via": "webhook",
  "correlation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "from_did": "5514981927913",
  "to_msisdn": "5514982151280",
  "extensionNumber": "6013",
  "call_type": "1",
  "source": "partner_api",
  "tenant_id": "cliente_bemmelhor",
  "dial_string": "cliente_bemmelhor-6013"
}

Campos importantes

  • correlation_id: UUID da ligação. Use para correlacionar com o Dial enriquecido (mesmo campo) e com a resposta do POST de calls.
  • to_msisdn: Número de destino da ligação WhatsApp.
  • from_did: DID ou WhatsApp de origem.
  • extensionNumber: Ramal da perna A (agente).
  • call_type: 1 oficial, 2 não oficial.
9

Transferência iniciada (TransferStarted)

Emitido quando uma transferência assistida é iniciada via API/softphone. O ramal destino começa a tocar; o agente permanece na ligação com o cliente até conclusão, cancelamento, falha ou timeout.

{
  "Event": "TransferStarted",
  "organizationId": "58126d91-27db-4195-bf71-d2c72041976b",
  "delivered_via": "webhook",
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transferMode": "attended",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "status": "ringing",
  "transferMethod": "",
  "error": "",
  "timestamp": "2026-08-13T18:00:00.000Z"
}

Campos importantes

  • transferId: UUID da transferência; use em cancel/status e nos demais eventos Transfer*.
  • extensionNumber: Ramal origem (agente que iniciou a transferência).
  • targetExtension: Ramal destino em consulta.
  • transferMode: Sempre attended (assistida) nesta API.
  • status: ringing no início; nos eventos finais: completed, failed, cancelled ou timeout.
10

Transferência concluída / falhou / cancelada / timeout

Eventos finais: TransferCompleted (destino atendeu e cliente foi bridged), TransferFailed (recusa/erro), TransferCancelled (agente cancelou) e TransferTimeout (destino não atendeu a tempo). Mesmo shape de TransferStarted, com status/error/transferMethod preenchidos quando aplicável.

{
  "Event": "TransferCompleted",
  "organizationId": "58126d91-27db-4195-bf71-d2c72041976b",
  "delivered_via": "webhook",
  "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transferMode": "attended",
  "extensionNumber": "6073",
  "targetExtension": "6010",
  "status": "completed",
  "transferMethod": "bridge_customer_target",
  "error": "",
  "timestamp": "2026-08-13T18:00:12.000Z"
}

Campos importantes

  • Event: TransferCompleted | TransferFailed | TransferCancelled | TransferTimeout
  • error: Mensagem legível em falha/timeout; vazio quando sucesso.
  • transferMethod: Método AMI usado na conclusão (ex.: bridge_customer_target), quando houver.
11

Ramal Conectado (ExtensionOnline)

Disparado quando um ramal registra no Asterisk (REGISTER). Cobre softphone WebRTC, iframe, MicroSIP, PortSIP e demais clientes SIP. Origem AMI: PeerStatus (Registered) ou ContactStatus (Created).

{
  "Event": "ExtensionOnline",
  "Extension": "6010",
  "Peer": "SIP/6010",
  "RegistrationStatus": "Registered",
  "AmiEvent": "PeerStatus",
  "ChannelType": "SIP",
  "Address": "192.168.1.10:5060"
}

Campos importantes

  • Extension: Número do ramal (4 dígitos).
  • Peer: Identificador do peer/endpoint no Asterisk.
  • RegistrationStatus: Status AMI original (Registered ou Created).
  • AmiEvent: Evento AMI de origem: PeerStatus (chan_sip) ou ContactStatus (PJSIP).
12

Ramal Desconectado (ExtensionOffline)

Disparado quando um ramal desregistra no Asterisk (UNREGISTER / contato removido). Logout do softphone, fechamento do cliente ou expiração do registro. Origem AMI: PeerStatus (Unregistered) ou ContactStatus (Removed).

{
  "Event": "ExtensionOffline",
  "Extension": "6010",
  "Peer": "SIP/6010",
  "RegistrationStatus": "Unregistered",
  "AmiEvent": "PeerStatus",
  "ChannelType": "SIP"
}

Campos importantes

  • Extension: Número do ramal (4 dígitos).
  • Peer: Identificador do peer/endpoint no Asterisk.
  • RegistrationStatus: Status AMI original (Unregistered ou Removed).
  • AmiEvent: Evento AMI de origem: PeerStatus (chan_sip) ou ContactStatus (PJSIP).

Softphone

Iframe do softphone Web

Incorpore o softphone da Bemmelhor no seu CRM com um link único por organização. O administrador define origens permitidas, teclado numérico e credenciais. No iframe, o usuário digita a senha, recebe um JWT e o softphone conecta ao ramal.

Visão geral do fluxo

  1. O administrador configura o link credenciado (origens permitidas, teclado de discagem) na aba Softphone (embed) do painel.
  2. O administrador cadastra credenciais; cada uma associa um usuário/ramal a uma senha que o operador digitará no iframe.
  3. O CRM incorpora o iframe com organizationId + embedId. Não é necessário passar JWT na URL.
  4. O usuário abre o iframe, informa a senha e o front-end chama embed-login. O JWT fica no sessionStorage da aba do iframe.
  5. Com o JWT válido, o softphone obtém credenciais SIP via embed-bootstrap e registra o ramal na rede.

Requisitos

  • Asterisk em modo PJSIP (stack nova) ou gateway legado com softphone WebRTC habilitado para a organização.
  • Ramal com senha SIP WebRTC configurada. Cada credencial de embed aponta para um ramal específico.
  • HTML do iframe com allow="microphone; autoplay" e referrerpolicy="strict-origin-when-cross-origin" para validar a origem do site pai quando houver allowedOrigins.

APIs de gestão (sessão administrador)

Autenticação por cookie de sessão Bemmelhor, papel de administrador da organização.

Link credenciado

  • GEThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/webrtc-softphone/organization-link

    Retorna link (iframeSrc, html, publicId, allowedOrigins, showDialpad), organization e eligibility.

  • PATCHhttps://service.bemmelhor.com.br/api/organizations/{organizationId}/webrtc-softphone/organization-link

    Corpo JSON opcional: allowedOrigins[], showDialpad?, isActive?.

Credenciais

  • GEThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/webrtc-softphone/credentials

    Lista credenciais com ramal e usuário vinculados.

  • POSThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/webrtc-softphone/credentials

    Corpo JSON: userExtensionId (UUID do ramal), password (senha do iframe), label?, isActive?.

  • PATCHhttps://service.bemmelhor.com.br/api/organizations/{organizationId}/webrtc-softphone/credentials/{credentialId}

    Atualiza password, label ou isActive.

  • DELETEhttps://service.bemmelhor.com.br/api/organizations/{organizationId}/webrtc-softphone/credentials/{credentialId}

    Remove a credencial; o usuário deixa de acessar o softphone com aquela senha.

Fluxo no iframe (usuário final)

Rotas públicas, sem cookie de sessão. O JWT fica no sessionStorage da aba do iframe.

  • GEThttps://service.bemmelhor.com.br/api/webrtc-softphone/organizations/{organizationId}/webrtc-softphone/embed-public-config?publicId={embedId}

    Carrega nome/logo da organização, elegibilidade do softphone e se o teclado está habilitado. Chamado antes do login.

  • POSThttps://service.bemmelhor.com.br/api/webrtc-softphone/organizations/{organizationId}/webrtc-softphone/embed-login

    Corpo: { "publicId", "password", "parentOrigin" }. Resposta: accessToken (JWT), expiresIn, extensionNumber, showDialpad e dados do usuário.

  • GEThttps://service.bemmelhor.com.br/api/webrtc-softphone/organizations/{organizationId}/webrtc-softphone/embed-bootstrap

    Cabeçalho Authorization: Bearer <accessToken>. Retorna URL WSS, credenciais SIP WebRTC, ICE servers e ramal.

Incorporar no CRM

Substitua {organizationId} e {embedId} pelos valores do painel (Segurança → Softphone embed).

https://app.bemmelhor.com.br/embed/webrtc-softphone?organizationId={organizationId}&embedId={embedId}

Pré-discagem a partir do CRM

Acrescente na query string tel, to ou number. O usuário ainda precisa estar autenticado no iframe.

https://app.bemmelhor.com.br/embed/webrtc-softphone?organizationId={organizationId}&embedId={embedId}&tel=5511999998888

Modo alternativo: sessão Bemmelhor

Reutiliza o login da Bemmelhor (cookie). Útil em intranet; em iframe de terceiros o navegador pode bloquear cookies. Prefira o fluxo credenciado com senha.

https://app.bemmelhor.com.br/embed/organizations/{organizationId}/softphone

Para Partner

Softphone na sua plataforma

Integre o softphone Bemmelhor no seu produto com um token PT. Use a Partner API, a API de integração (calls, gravações e CRUD de Telefonia & URA) nas orgs vinculadas e o WSS de softphone, sem receber a senha SIP.

1

Receba o token PT

A Bemmelhor gera o token no painel interno. Guarde apenas no seu backend.

2

Crie uma sessão

Chame POST softphone-sessions com organização e ramal. Você recebe um JWT curto e a URL do proxy.

3

Conecte no proxy e registre

Abra o WSS do proxy com ?token= e faça REGISTER. Sem REGISTER não há toque de entrada.

4

Saída e entrada

Saída PSTN: Inviter.invite(). Entrada: delegate.onInvite (SIP INVITE). WhatsApp: REST Bridge + atender o INVITE da perna A.

Boas práticas

  • Token PT só no backend. Nunca no browser do usuário final.
  • Senha SIP nunca é devolvida pela Partner API / sessão softphone.
  • No POST Bridge /extensions com secret:auto, a senha gerada pode vir na resposta 201: guarde e não logue em claro.
  • Áudio WebRTC vai direto ao Asterisk. O proxy só intermedia a sinalização.
  • Transferência assistida via REST (Partner ou Integrations), não via SIP REFER no proxy. Hold/segunda linha seguem fora do escopo.
  • Eventos Transfer* no webhook da org e em /ws/telephony-events (mesmo JWT da sessão). O /ws/softphone-proxy continua só SIP.
  • PT também acessa /api/integrations/v1 só nas orgs da allowlist (header X-Organization-Id).
  • Status em tempo real dos ramais (online/offline, idle/ringing/in_call/dialing): GET /api/integrations/v1/extensions (scope extensions:status; PT já cobre com *).
  • CRUD de ramais, filas, áudios e URAs (Telefonia & WhatsApp / Bridge): /api/integrations/v1/telephony-bridge, só orgs com integração partner e setup Bridge (não use /api/organizations/...).
  • WhatsApp oficial/não oficial: POST telephony-bridge/calls. PSTN: INVITE no WSS.

Autenticação

Base da API: https://service.bemmelhor.com.br/api/partner/v1. Use o token PT-<SIGLA>-<segredo> em todas as requisições.

Header preferido

Authorization: Bearer <token>

Alternativa

x-api-key: <token>
curl "https://service.bemmelhor.com.br/api/partner/v1/organizations" \
  -H "Authorization: Bearer PT-ACME-seu_segredo"

API de integração

O mesmo token PT-… autentica a API de integração em https://service.bemmelhor.com.br/api/integrations/v1, apenas nas organizações vinculadas ao partner. Informe a org alvo com X-Organization-Id (obrigatório se houver mais de uma org). Scopes equivalentes a * dentro dessas orgs. Inclui calls, gravações, click-to-call e o CRUD de Telefonia & URA.

curl "https://service.bemmelhor.com.br/api/integrations/v1/calls?page=1&limit=20" \
  -H "Authorization: Bearer PT-ACME-seu_segredo" \
  -H "X-Organization-Id: 25baa84c-5afd-4f32-9417-5de758aaf4cd"

CRUD Telefonia & URA

Com o token PT-… você pode criar, listar, atualizar e excluir ramais, filas, áudios, URAs e DIDs via Bridge: somente nas organizações da allowlist com integração partner / Telefonia & WhatsApp. Use a API de integração, não as rotas /api/organizations/... (retornam 403 para PT). Para status em tempo real dos ramais (sem Bridge), use GET /api/integrations/v1/extensions.

Base: https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge. Pré-requisito: org com Bridge configurado. Listagens usam envelope { items, total }. DELETE bem-sucedido → 204 sem corpo. Catálogo completo: seção Telefonia & URA.

Auth

Authorization: Bearer PT-…

Org (multi-org)

X-Organization-Id: <uuid>
RecursoTipoEndpoints
StatusGEThttps://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/status
RamaisCRUDGET/POST /extensions · GET/PATCH/DELETE /extensions/:ext
FilasCRUDGET/POST /queues · GET/PATCH/DELETE /queues/:name · POST /queues/:name/members
ÁudiosCRUDGET/POST /audios · GET .../voices · GET/PATCH/PUT/DELETE /audios/:audioId · GET .../raw
URAs (IVR)CRUDGET/POST /ivrs · GET/PATCH/DELETE /ivrs/:ivrId · GET .../ivrs/:ivrId/dids
DIDsCRUDGET/POST /dids · GET/PATCH/DELETE /dids/:number

Entrada e saída (exemplos)

POSTCriar ramal201
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions

Cria ramal no Bridge. Com secret: "auto", a resposta pode incluir a senha gerada: guarde com segurança (só aparece na criação).

  • extension: 3 a 8 dígitos. transport: wss | udp.
  • DELETE /extensions/:ext → 204 sem corpo.
{
  "extension": "6001",
  "display_name": "Maria Silva",
  "transport": "wss",
  "secret": "auto",
  "default_queue": null
}
GETListar ramais200
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions

Listagens Bridge usam envelope { items, total }. Query opcional: status=online|offline.

{
  "items": [
    {
      "tenant_id": "acme",
      "extension": "6001",
      "display_name": "Maria Silva",
      "transport": "wss",
      "status": "online",
      "default_queue": "vendas",
      "sip_username": "acme-6001"
    }
  ],
  "total": 1
}
POSTCriar fila201
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues

Cria fila. strategy comum: rrmemory ou ringall.

{
  "name": "vendas",
  "display_name": "Vendas",
  "strategy": "rrmemory"
}
POSTAlterar membros da fila200
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/queues/vendas/members

Adiciona/remove ramais. Para ver membros, use GET /queues/:name (não existe GET .../members).

{
  "add": [
    "6001"
  ],
  "remove": [
    "6002"
  ]
}
POSTCriar áudio (TTS)201
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios

Síntese de voz. Consulte GET /audios/voices para vozes válidas (ex.: francisca, antonio, leila).

  • Upload: multipart/form-data com audio_id + file (MP3/WAV, até ~10 MB).
  • Quota padrão: 100 áudios/org → 409 se exceder.
{
  "audio_id": "saudacao_entrada",
  "source": "tts",
  "text": "Bem-vindo ao atendimento. Digite a opção desejada.",
  "voice": "francisca",
  "rate": "+0%",
  "pitch": "+0Hz",
  "description": "Saudação da URA"
}
GETListar vozes TTS200
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/audios/voices

Vozes disponíveis para source=tts.

{
  "items": [
    {
      "voice": "francisca",
      "label": "Francisca"
    },
    {
      "voice": "antonio",
      "label": "Antonio"
    },
    {
      "voice": "leila",
      "label": "Leila"
    }
  ]
}
POSTCriar URA201
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/ivrs

Actions de opção usuais: queue | ivr | extension | hangup. target é o destino (fila, outra URA ou ramal).

{
  "ivr_id": "ura_principal",
  "display_name": "URA Principal",
  "language": "pt_BR",
  "greeting_audio_id": "saudacao_entrada",
  "menu_audio_id": "menu_opcoes",
  "options": [
    {
      "digit": "1",
      "action": "queue",
      "target": "vendas",
      "voice_keywords": [
        "vendas",
        "comercial"
      ]
    },
    {
      "digit": "2",
      "action": "extension",
      "target": "6001"
    },
    {
      "digit": "9",
      "action": "hangup"
    }
  ],
  "allow_direct_extension_dial": true,
  "extension_dial_patterns": [
    "6XXX"
  ],
  "voice_recognition": {
    "enabled": false,
    "engine": "whisper_local",
    "max_record_seconds": 6
  },
  "timeout_config": {
    "wait_dtmf_seconds": 5,
    "max_tries": 3,
    "on_timeout_action": "hangup"
  },
  "invalid_input_action": "replay"
}
PATCHApontar DID para URA200
https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/dids/5514981927913

Altera a URA de destino. Use ivr_id: null para desvincular. number: 10 a 15 dígitos.

{
  "ivr_id": "ura_principal"
}

Erros comuns (JSON)

400Partner multi-org sem X-Organization-Id (API de integração)
{
  "error": "organizationId é obrigatório para token de partner (header x-organization-id, query ou body)"
}
400Sessão sem ramal
{
  "error": "Informe extensionNumber ou userExtensionId"
}
401Token inválido ou inativo
{
  "error": "Credenciais inválidas ou expiradas"
}
403Org fora da allowlist
{
  "error": "Partner sem acesso a esta organização"
}
403Softphone desabilitado na org
{
  "error": "Softphone WebRTC não está habilitado para esta organização"
}
409Quota de áudios
{
  "error": "Limite de áudios atingido para esta organização"
}
409WhatsApp: permissão pendente (não é ligação em andamento)
{
  "code": "whatsapp_permission_pending",
  "permission_required": true,
  "permission": {
    "from_did": "551140028922",
    "to_msisdn": "5511987654321",
    "status": "pending",
    "permission_type": "temporary",
    "gupshup_message_id": "gupshup-msg-abc123",
    "requested_at": "2026-08-07T18:10:00.000Z",
    "expires_at": null,
    "tenant_id": "acme"
  },
  "message": "Solicitação de permissão enviada ao cliente no WhatsApp. Aguarde a resposta e tente ligar novamente.",
  "error": "Solicitação de permissão enviada ao cliente no WhatsApp. Aguarde a resposta e tente ligar novamente."
}
422WhatsApp sem DID
{
  "error": "Nenhum DID cadastrado para este tenant. Cadastre um DID antes de ligar via WhatsApp."
}

Canais de discagem

O softphone da plataforma escolhe o canal na UI e bifurca o caminho. No partner é o mesmo: PSTN pelo WSS, WhatsApp pelo POST Bridge. Consulte capabilities para saber o que a org tem habilitado.

PSTN

Crie a sessão softphone, registre no WSS e disque com SIP INVITE (sip:NUMERO@dominio).

WhatsApp oficial

Consulte capabilities, mantenha o ramal registrado no WSS e chame POST telephony-bridge/calls com call_type=1. Em 409 permission pending, mostre a mensagem e aguarde o cliente aceitar no WhatsApp.

WhatsApp não oficial

Igual ao oficial, com call_type=2. Não envie INVITE SIP para originar a chamada WhatsApp.

Parâmetros do POST de ligação WhatsApp

NomeLocalTipoDescrição
AuthorizationreqheaderstringBearer PT-…
organizationIdreqpathuuidOrganização na allowlist do partner.
extensionNumberreqbodystringRamal que vai atender a perna A (deve existir na org).
to_msisdnreqbodystringDestino só com dígitos. No oficial (Meta), o 9 de celular é normalizado automaticamente.
call_typereqbodynumber1 = WhatsApp oficial (Meta). 2 = WhatsApp não oficial.
from_didbodystringDID de origem. Se omitido, usa o primeiro DID da org (via capabilities).

Request / response

curl "https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/telephony-bridge/capabilities" \
  -H "Authorization: Bearer PT-ACME-seu_segredo"

No oficial (Meta), to_msisdn na resposta pode vir sem o 9º dígito. Em 409 com whatsapp_permission_pending, mostre message e não trate como chamada ativa.

POSThttps://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/softphone-sessions

Criar sessão softphone

Cria uma sessão curta para um ramal. Informe extensionNumber ou userExtensionId. A resposta traz JWT, metadados SIP e a URL do proxy. A senha SIP nunca é devolvida.

Parâmetros

NomeLocalTipoDescrição
AuthorizationobrigatórioheaderstringToken PT no formato Bearer. Alternativa: header x-api-key com o mesmo valor.ex.: Bearer PT-ACME-seu_segredo
organizationIdobrigatóriopathuuidID da organização. Precisa estar na allowlist do partner.ex.: 25baa84c-5afd-4f32-9417-5de758aaf4cd
extensionNumberopcionalbodystringNúmero do ramal (ex.: 6001). Obrigatório se userExtensionId não for enviado. Informe um dos dois.ex.: 6001
userExtensionIdopcionalbodyuuidID do vínculo usuário-ramal. Obrigatório se extensionNumber não for enviado. Informe um dos dois.ex.: a1b2c3d4-e5f6-7890-abcd-ef1234567890

Regra: envie pelo menos um entre extensionNumber e userExtensionId. Se enviar os dois, ambos precisam apontar para o mesmo ramal.

Corpo de exemplo

{
  "extensionNumber": "6001"
}

Resposta 201

CampoTipoDescrição
accessTokenstring (JWT)Token da sessão. Use no WSS do proxy como ?token=... Expira em expiresIn segundos.
expiresInnumberValidade do accessToken em segundos (padrão: 900).
tokenTypestringSempre "Bearer".
sessionIduuidIdentificador da sessão criada.
organizationIduuidOrganização da sessão.
extensionNumberstringRamal resolvido para a sessão.
sipDomainstringDomínio SIP para montar as URIs no SIP.js.
sipAuthUsernamestringUsername SIP do ramal (ex.: 6001-webrtc). Use no REGISTER; a senha fica vazia no cliente.
iceServersarrayServidores ICE/STUN para a mídia WebRTC.
softphoneProxyUrlstring (wss)URL do proxy SIP over WebSocket. Conecte o SIP.js aqui, não no Asterisk.
telephonyEventsUrlstring (wss)WebSocket JSON de eventos de telefonia (ex.: Transfer*). Use o mesmo accessToken: wss://…/ws/telephony-events?token=. Não misture com o proxy SIP.

Erros comuns

StatusQuando
400Bad RequestBody inválido, ou nenhum de extensionNumber / userExtensionId informado.
401UnauthorizedToken PT ausente, inválido ou inativo.
403ForbiddenPartner sem acesso à organização, softphone desabilitado, ou senha WebRTC do ramal não cadastrada.
404Not FoundOrganização ou ramal não encontrado.
503Service UnavailableConfiguração SIP WebRTC da organização incompleta.
curl -X POST "https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/softphone-sessions" \
  -H "Authorization: Bearer PT-ACME-seu_segredo" \
  -H "Content-Type: application/json" \
  -d '{"extensionNumber":"6001"}'

Proxy SIP (WSS) e ligações

Conecte o SIP.js no proxy Bemmelhor, nunca no Asterisk. Passe o JWT da sessão na query ?token=. Deixe a senha SIP vazia: o proxy autentica no hop interno.

Proxy: wss://service.bemmelhor.com.br/ws/softphone-proxy

Como funciona o proxy SIP

O softphone Partner não fala SIP diretamente com o Asterisk. O navegador (ou o seu cliente SIP.js) abre um WebSocket no proxy Bemmelhor (`/ws/softphone-proxy`). O proxy valida o JWT da sessão, abre um segundo WebSocket até o Asterisk e retransmite a sinalização SIP nos dois sentidos.

A senha SIP fica só no backend/proxy: o cliente envia REGISTER/INVITE sem Authorization; se o Asterisk responder 401/407, o proxy injeta o Digest e reenvia. Por isso `authorizationPassword` no SIP.js deve ficar vazio.

O áudio WebRTC (RTP via ICE) vai direto entre o browser e o Asterisk. O proxy só intermedia sinalização SIP over WebSocket e não muda o codec nem roteia mídia.

Caminho da sinalização

SIP.js (browser) ↔ WSS proxy Bemmelhor ↔ WSS Asterisk

Caminho do áudio

Browser (WebRTC/ICE) ↔ Asterisk (sem passar pelo proxy)

Keep-alive do SIP.js (evita close 1006 por idle)

O SIP.js, por padrão, não envia keep-alive no WebSocket (`keepAliveInterval: 0`). Sem tráfego periódico, proxies e load balancers costumam encerrar a conexão ociosa em cerca de 60 segundos. O browser reporta isso como WebSocket close code 1006 (fechamento anormal) e o SIP.js dispara `onDisconnect` / transport disconnected.

Configure `transportOptions.keepAliveInterval` (em segundos) para o SIP.js enviar sequências CRLF no WSS e manter a sessão viva. Use um valor menor que o idle timeout da infra (recomendado: 20 a 30). Opcionalmente ajuste `keepAliveDebounce` (padrão 10).

Isso é independente do `Expires` do REGISTER e do `expiresIn` do JWT da sessão. O keep-alive só evita idle no socket; não renova o token nem substitui o re-REGISTER.

const ua = new UserAgent({
  uri,
  transportOptions: {
    server: `${session.softphoneProxyUrl}?token=${encodeURIComponent(session.accessToken)}`,
    // Intervalo em SEGUNDOS entre CRLF keep-alive no WebSocket.
    // Default do SIP.js = 0 (desligado) → WSS ocioso cai ~60s com code 1006.
    keepAliveInterval: 30, // use valor < timeout de idle da infra (ex.: 60s)
    keepAliveDebounce: 10,
  },
  authorizationUsername: session.sipAuthUsername,
  authorizationPassword: "",
});

JWT da softphone-session (TTL 15 min)

O `accessToken` da sessão Partner dura cerca de 15 minutos (`expiresIn: 900`). Use esse valor (ou o `exp` do JWT) para agendar renovação. Keep-alive e re-REGISTER SIP não renovam o token.

Para ficar online por mais tempo: antes do `exp`, chame de novo `POST .../softphone-sessions`, faça dispose limpo do UserAgent/Registerer e reconecte no proxy com o novo `accessToken`. Deixe margem (ex.: 60s antes do exp).

Se o JWT já expirou, uma nova abertura de WSS com o token antigo falha com close 1008 (“Sessão inválida ou expirada”). Quedas longas (~15–20 min) com keepalives ainda ok até perto do fim costumam ser lifetime da sessão — não o idle de ~60s.

No `onDisconnect`, chame `Registerer.dispose()` (e `UserAgent.stop()`). Só zerar a ref deixa o timer de refresh ativo: minutos depois aparece REGISTER “zumbi” com transporte `Disconnected` / 503 — efeito colateral, não uma segunda queda real.

// session.expiresIn === 900 (15 min). Renove ANTES do exp.
const RENEW_MARGIN_MS = 60_000; // 1 min de folga

function scheduleSessionRenewal(session, connectSip) {
  const delay = Math.max(0, session.expiresIn * 1000 - RENEW_MARGIN_MS);
  return setTimeout(() => {
    void (async () => {
      // 1) Nova softphone-session (JWT fresco)
      const next = await createSoftphoneSession(/* org + ramal */);
      // 2) Dispose limpo + reconnect no proxy com o novo token
      await disposeSip();
      await connectSip(next);
    })();
  }, delay);
}

async function disposeSip() {
  const registerer = registererRef.current;
  registererRef.current = null;
  if (registerer) {
    try {
      await registerer.dispose(); // para timers de re-REGISTER
    } catch {
      /* ignore */
    }
  }

  const ua = userAgentRef.current;
  userAgentRef.current = null;
  if (ua) {
    try {
      await ua.stop();
    } catch {
      /* ignore */
    }
  }
}

// No onDisconnect / TransportState.Disconnected:
// NÃO só zere as refs — chame disposeSip(), senão o Registerer
// continua com timer e tenta REGISTER com transporte morto → 503.

Re-REGISTER e refreshFrequency (não confundir com 1006)

O Asterisk/proxy costuma aceitar o registro com `Expires: 120` (não 600). O SIP.js, por padrão, dispara o re-REGISTER em cerca de 99% desse intervalo (~119s). Se a resposta 200 do refresh demorar alguns segundos, o timer local do cliente pode marcar `Registration expired` / `RegistererState.Unregistered` por uma janela curta (ex.: 4s) até o 200 chegar e voltar a `Registered`.

Isso é corrida de timer SIP, não queda do WebSocket: keepalives e OPTIONS podem continuar normais nesse intervalo. Em teoria, um INVITE inbound nessa janela pode falhar no client.

Ajuste `refreshFrequency` no `Registerer` (porcentagem do Expires, tipicamente 50 a 99). Com `expires: 120` e `refreshFrequency: 80`, o refresh sai ~aos 96s e sobra folga para o 200. Também: `register()` resolve ao enviar o pedido; espere `RegistererState.Registered` antes de considerar a linha pronta.

import { UserAgent, Registerer, RegistererState } from "sip.js";

// register() resolve quando o pedido é ENVIADO, não quando o 200 chega.
// Espere RegistererState.Registered antes de considerar a linha pronta.
const registerer = new Registerer(ua, {
  expires: 120,
  // % do Expires em que o re-REGISTER é disparado (50–99).
  // Default ~99 com Expires 120 → renova ~aos 119s.
  // Se o 200 demorar alguns segundos, o timer local marca Unregistered
  // por uma janela curta (WSS continua de pé; keepalives/OPTIONS seguem).
  // 80 → renova ~aos 96s e evita essa corrida.
  refreshFrequency: 80,
  logConfiguration: false,
});

await new Promise((resolve, reject) => {
  const timeout = setTimeout(() => {
    reject(new Error("Timeout aguardando REGISTER"));
  }, 15000);

  registerer.stateChange.addListener((state) => {
    if (state === RegistererState.Registered) {
      clearTimeout(timeout);
      resolve(undefined);
    }
    // Unregistered no meio de um refresh lento: não trate como queda do WSS.
    // Espere o próximo Registered ou só reconecte se persistir.
  });

  void registerer.register().catch((err) => {
    clearTimeout(timeout);
    reject(err);
  });
});

Receber ligação: não há evento JSON próprio

Não existe mensagem tipo `{ "event": "incoming_call" }` no WSS do proxy. A Bemmelhor não publica um canal de eventos de chamada nesse socket.

Depois que o ramal faz REGISTER com sucesso, o Asterisk trata o softphone como endpoint SIP online. Qualquer ligação para esse ramal (PSTN entrante, URA, transferência, ou perna A de uma chamada WhatsApp originada via Bridge) chega como um SIP INVITE padrão, encaminhado pelo proxy até o seu SIP.js.

No SIP.js, isso aparece no delegate `onInvite` do `UserAgent` (objeto `Invitation`). Na UI: mostrar “chamando…”, tocar ringtone, e chamar `invitation.accept()` ou `invitation.reject()`.

Se já houver outra sessão ativa, rejeite com 486 (Busy Here). O softphone Partner/embed só suporta uma chamada por vez.

  1. 1. Abrir o WSS do proxyUse `softphoneProxyUrl` + `?token=` com o JWT da sessão (`accessToken`). Sem token válido o proxy fecha a conexão (código 1008).
  2. 2. REGISTERRegistre o `sipAuthUsername` no domínio da sessão. Só após REGISTER o Asterisk consegue entregar INVITEs de entrada para esse ramal.
  3. 3. Esperar INVITE (entrada)Implemente `delegate.onInvite`. Não poll REST e não espere webhook nesse WSS: a sinalização de toque é o próprio INVITE SIP.
  4. 4. Aceitar / recusar`accept()` estabelece a sessão e o áudio remoto; `reject({ statusCode: 486 })` se estiver ocupado. Em `SessionState.Terminated`, limpe a UI.

Originar ligação

  • PSTN (telefone)Com o ramal registrado, crie um `Inviter` para `sip:{numero}@{sipDomain}` e chame `invite()`. O número tipicamente vai em E.164 sem `+` (ex.: 5511999998888).
  • WhatsApp (oficial / não oficial)Não use INVITE SIP para originar. Chame `POST /api/integrations/v1/telephony-bridge/calls` com o ramal já registrado no WSS. A Bridge cria a chamada; o softphone recebe a perna do agente como INVITE (mesmo fluxo de `onInvite` / atender).

Códigos comuns de fechamento do WSS

CódigoQuando
1006Fechamento anormal (sem close frame). Idle ~60s sem keepAliveInterval; ou queda após ~15–20 min quando a softphone-session (JWT) já expirou e o socket cai depois — keepalive não renova o token
1008Token ausente, sessão inválida/expirada (JWT), partner inativo, token revogado, org sem vínculo ou ramal inválido — típico ao (re)abrir o WSS com token velho
1011Falha interna ao resolver credenciais SIP / conectar ao Asterisk
1000Fechamento normal (cliente desconectou ou tear-down limpo)

Perguntas frequentes

O proxy emite eventos AMI (Dial, Hangup, etc.)?
Não. AMI/webhooks são outro canal (`/api/.../webhooks`). O WSS do softphone só transporta SIP (REGISTER, INVITE, ACK, BYE, etc.).
Preciso conectar no WSS do Asterisk?
Não. Sempre use `softphoneProxyUrl` da sessão. Expor o WSS/senha do Asterisk ao browser é inseguro e fora do contrato Partner.
Por que a ligação entrante não toca?
Confirme REGISTER ativo, JWT ainda válido, softphone WebRTC habilitado na org, e que o destino da chamada é o ramal da sessão. Sem REGISTER, o Asterisk não entrega o INVITE ao softphone.
Por que o WSS cai com code 1006 após ~60 segundos?
Quase sempre idle timeout: o SIP.js vem com keepAliveInterval: 0 (desligado). Sem CRLF periódico, proxy/LB fecha o socket ocioso. Configure transportOptions.keepAliveInterval (ex.: 30) e REGISTER com expires 120 + refreshFrequency 80.
Por que cai com 1006 depois de ~15–20 minutos, com keepalive ok?
Lifetime da softphone-session: o JWT dura ~15 min (expiresIn 900). Keepalive mantém o TCP quente, mas não renova o token. Renove com POST softphone-sessions + reconnect antes do exp (com margem). Reabrir WSS com JWT expirado → 1008.
Vi REGISTER 503 / Not connected minutos após o disconnect. É outra queda?
Em geral não: é timer do Registerer ainda vivo. No onDisconnect chame registerer.dispose() e userAgent.stop(); não só zere as refs. Sem dispose, o refresh tenta REGISTER com transporte Disconnected.
Vi Unregistered / Registration expired mas o WebSocket não caiu. É o 1006?
Não. Com Expires 120 e refresh em 99%, o re-REGISTER sai ~aos 119s; se o 200 atrasar, o timer local marca Unregistered por alguns segundos e depois volta a Registered. Use refreshFrequency: 80 (renova ~aos 96s). Espere RegistererState.Registered; register() só indica que o pedido foi enviado.
import {
  UserAgent,
  Registerer,
  SessionState,
  Invitation,
} from "sip.js";

// session = resposta do POST softphone-sessions
// remoteAudio = <audio autoPlay playsInline /> no DOM
let activeSession: Invitation | null = null;

const uri = UserAgent.makeURI(
  `sip:${session.sipAuthUsername}@${session.sipDomain}`
);

const ua = new UserAgent({
  uri,
  transportOptions: {
    server: `${session.softphoneProxyUrl}?token=${encodeURIComponent(session.accessToken)}`,
    // Evita close 1006 por idle (~60s) em proxy/LB/Cloudflare
    keepAliveInterval: 30,
    keepAliveDebounce: 10,
    connectionTimeout: 15,
  },
  authorizationUsername: session.sipAuthUsername,
  authorizationPassword: "",
  sessionDescriptionHandlerFactoryOptions: {
    peerConnectionConfiguration: {
      iceServers: session.iceServers,
    },
  },
  delegate: {
    onInvite: (invitation: Invitation) => {
      // Uma chamada por vez: se já houver sessão, recuse
      if (activeSession) {
        void invitation.reject({ statusCode: 486 }); // Busy Here
        return;
      }

      activeSession = invitation;

      // UI: mostrar "Chamando...", callerId, botões Atender / Recusar
      const from = invitation.remoteIdentity.uri.toString();
      console.log("Ligação entrante:", from);

      invitation.stateChange.addListener((state) => {
        if (state === SessionState.Established) {
          const sdh = invitation.sessionDescriptionHandler as {
            remoteMediaStream?: MediaStream;
          } | undefined;
          if (sdh?.remoteMediaStream && remoteAudio) {
            remoteAudio.srcObject = sdh.remoteMediaStream;
            void remoteAudio.play().catch(() => {});
          }
        }

        if (state === SessionState.Terminated) {
          activeSession = null;
          if (remoteAudio) remoteAudio.srcObject = null;
          // UI: voltar para "registrado" / idle
        }
      });
    },
  },
});

await ua.start();
await new Registerer(ua, {
  expires: 120,
  refreshFrequency: 80,
}).register();

// Chamado pelos botões da UI quando houver activeSession (toque)
async function acceptIncomingCall() {
  if (!activeSession) return;
  await activeSession.accept();
}

async function rejectIncomingCall() {
  if (!activeSession) return;
  await activeSession.reject({ statusCode: 486 });
  activeSession = null;
}

async function hangup() {
  if (!activeSession) return;
  await activeSession.bye();
  activeSession = null;
}

Limites

  • Uma chamada por vez no softphone (como no embed).
  • Sessão softphone (JWT) expira em ~15 min (expiresIn 900). Renove com POST softphone-sessions + reconnect antes do exp; keepalive não renova o token.
  • Token revogado ou regenerado bloqueia novas sessões e encerra conexões na revalidação.
  • Configure keepAliveInterval (ex.: 30s) no SIP.js; sem isso o WSS ocioso tende a cair ~60s com code 1006.
  • No Registerer use expires: 120 e refreshFrequency: 80 para evitar Unregistered breve na renovação (corrida de timer, não queda do WSS).
  • No onDisconnect: Registerer.dispose() + UserAgent.stop() (não só limpar refs), senão o refresh dispara REGISTER zumbi / 503 com WSS morto.
  • WhatsApp oficial pode exigir permissão do cliente antes de completar.
  • Se POST telephony-bridge/calls retornar HTTP 409 com code=whatsapp_permission_pending, pare a UI de discagem e mostre message. Não é ligação em andamento e não deve retentar em loop.

Endpoints Partner API

Base https://service.bemmelhor.com.br/api/partner/v1. Todos exigem o token PT e respeitam a allowlist. Abaixo: request e response de exemplo por rota.

  • GETListar organizações200
    https://service.bemmelhor.com.br/api/partner/v1/organizations

    Retorna as organizações que o seu token pode acessar, com flags de softphone.

    {
      "organizations": [
        {
          "id": "25baa84c-5afd-4f32-9417-5de758aaf4cd",
          "name": "Acme Telecom",
          "avatarUrl": "https://cdn.exemplo.com/orgs/acme.png",
          "webrtcBrowserSoftphoneEnabled": true,
          "webrtcSoftphoneMode": "pjsip"
        }
      ]
    }
  • GETListar ramais (softphone)200
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/extensions

    Lista ramais da organização para softphone. Indica se há senha WebRTC cadastrada, sem expor a senha SIP. Não é o CRUD Bridge.

    • hasWebrtcSipPassword=false impede criar sessão até um admin cadastrar a senha.
    {
      "softphoneEnabled": true,
      "extensions": [
        {
          "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "extensionNumber": "6001",
          "hasWebrtcSipPassword": true,
          "user": {
            "id": "11111111-2222-3333-4444-555555555555",
            "name": "Maria Silva",
            "email": "maria@acme.com"
          }
        },
        {
          "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
          "extensionNumber": "6002",
          "hasWebrtcSipPassword": false,
          "user": null
        }
      ]
    }
  • GETListar contatos200
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/contacts

    Busca paginada. Query: search, page, limit.

    {
      "contacts": [
        {
          "id": "c0ffee00-0000-4000-8000-000000000001",
          "name": "João Cliente",
          "phones": [
            {
              "id": "p1111111-1111-4111-8111-111111111111",
              "phoneNumber": "5511987654321"
            }
          ]
        }
      ],
      "total": 1,
      "page": 1,
      "limit": 20
    }
  • POSTCriar sessão softphone201
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/softphone-sessions

    Emite JWT de sessão (~15 min). Informe extensionNumber ou userExtensionId. Não devolve senha SIP.

    {
      "extensionNumber": "6001"
    }
  • GETCapabilities (canais)200
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/telephony-bridge/capabilities

    Lista canais habilitados (pstn, whatsapp_official, whatsapp_unofficial) e DIDs da org.

    {
      "tenant_id": "acme",
      "call_types": [
        {
          "code": 0,
          "name": "pstn",
          "label": "Telefone (PSTN)",
          "enabled": true,
          "reason": null
        },
        {
          "code": 1,
          "name": "whatsapp_official",
          "label": "WhatsApp Oficial",
          "enabled": true,
          "reason": null
        },
        {
          "code": 2,
          "name": "whatsapp_unofficial",
          "label": "WhatsApp Não Oficial",
          "enabled": false,
          "reason": "Canal não configurado para este tenant"
        }
      ],
      "dids": [
        "551140028922",
        "5514981927913"
      ]
    }
  • POSTLigação WhatsApp202
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/telephony-bridge/calls

    Origina ligação WhatsApp oficial (call_type=1) ou não oficial (call_type=2). PSTN usa WSS INVITE. Sucesso típico: HTTP 202.

    • No oficial (Meta), o 9º dígito do celular pode ser normalizado na resposta (to_msisdn).
    • HTTP 409 com code=whatsapp_permission_pending: mostre message e não retente em loop.
    {
      "extensionNumber": "6001",
      "to_msisdn": "5511987654321",
      "call_type": 1,
      "from_did": "551140028922"
    }
  • POSTTransferência assistida202
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/softphone/supervised-transfer

    Com o ramal em ligação, inicia transferência assistida: o destino toca; só após atender o cliente é bridged. Emite TransferStarted (webhook + /ws/telephony-events).

    • Cancelar: POST .../softphone/supervised-transfer/cancel com { transferId }.
    • Status: GET .../softphone/supervised-transfer/{transferId} (ringing, completed, failed, cancelled, timeout).
    • Eventos: TransferStarted, TransferCompleted, TransferFailed, TransferCancelled, TransferTimeout.
    • Erros: 400 ramal sem ligação / ramais iguais / canal do cliente; 404 ramal ou Asterisk; 409 já existe assistida em andamento; 401/403 autenticação e escopo.
    {
      "extensionNumber": "6001",
      "targetExtension": "6002"
    }
  • POSTCancelar transferência assistida200
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/softphone/supervised-transfer/cancel

    Encerra a consulta ao destino; a ligação permanece com o agente. Emite TransferCancelled.

    • 200 também se a transferência já finalizou: message indica o status final (completed, failed, cancelled, timeout).
    • Erros: 400 transferId inválido; 403 transferência não pertence a este ramal; 404 não encontrada.
    {
      "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "extensionNumber": "6001"
    }
  • GETStatus da transferência assistida200
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/softphone/supervised-transfer/{transferId}

    Consulta o status (ringing, completed, failed, cancelled, timeout).

    • status: ringing | completed | failed | cancelled | timeout.
    • Em failed/timeout o campo error pode vir preenchido.
    • Erros: 400 transferId obrigatório; 404 transferência não encontrada.
    {
      "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "status": "completed",
      "extensionNumber": "6001",
      "targetExtension": "6002",
      "transferMethod": "bridge_customer_target",
      "message": "Transferência assistida concluída"
    }

Pronto para integrar?

Acesse o painel para gerar chaves de API, configurar webhooks e copiar o link do softphone da sua organização.