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

  • queues:pause

    Pausar ramal (fila ou DND)

  • missed_calls:read

    Chamadas perdidas, leitura

  • missed_calls:write

    Chamadas perdidas, marcar como vistas

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: ligação curta volta na mesma resposta; a longa responde 202 (em andamento) e é só consultar de novo.

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.

Respostas

200OK

Trechos com falante ("0", "1"…), início e fim em segundos. generated diz se foi gerada agora.

{
  "transcriptions": [
    {
      "sequence": "1",
      "startAt": 0.4,
      "endAt": 5.1,
      "speaker": "0",
      "text": "Bom dia, em que posso ajudar?"
    }
  ],
  "generated": false
}
202Em andamento

A transcrição ainda está sendo gerada (ligação longa). Consulte de novo depois do tempo do header Retry-After (15 s); pedidos repetidos acompanham a mesma geração, sem gerar de novo.

{
  "status": "processing",
  "message": "Transcrição em andamento. Consulte de novo em alguns segundos.",
  "startedAt": "2026-10-20T15:07:35.060Z"
}
403Sem permissão

Chave sem o escopo, ou integração de transcrição desabilitada na organização.

{
  "error": "Integração de transcrição não está habilitada para esta organização"
}
404Não encontrada

Ligação inexistente, sem gravação ou sem uniqueid.

{
  "error": "Ligação não encontrada ou não possui uniqueid"
}
429Limite atingido

Limite diário de transcrição da organização atingido (code transcription_daily_limit_reached; o consumo zera à meia-noite de Brasília e o header Retry-After diz em quantos segundos), ou a mesma ligação já foi transcrita o máximo de vezes em 24 h. Não repita o pedido antes do Retry-After.

{
  "error": "Limite de transcrição diário atingido. O consumo com o painel da BemMelhor excedeu o diário permitido. Para aumentar, entre em contato com o suporte: (32) 3211-0536.",
  "code": "transcription_daily_limit_reached",
  "retryAfterSeconds": 25200
}
500Falha ao transcrever

Erro na geração. Quando é falta de crédito no serviço de transcrição, a mensagem diz isso.

{
  "error": "Erro ao transcrever áudio (openai): Sem créditos na conta OpenAI (credit_balance_exhausted)."
}

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: ligação curta volta na mesma resposta; a longa responde 202 (em andamento) e é só consultar de novo.

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.

Respostas

200OK

Resumo da ligação. generated diz se foi gerado agora.

{
  "summary": "Cliente pediu para remarcar a consulta para segunda às 9h30.",
  "generated": true
}
202Em andamento

A transcrição ainda está sendo gerada (ligação longa). Consulte de novo depois do tempo do header Retry-After (15 s); pedidos repetidos acompanham a mesma geração, sem gerar de novo.

{
  "status": "processing",
  "message": "Transcrição em andamento. Consulte de novo em alguns segundos.",
  "startedAt": "2026-10-20T15:07:35.060Z"
}
403Sem permissão

Chave sem o escopo, ou integração de transcrição desabilitada na organização.

{
  "error": "Integração de transcrição não está habilitada para esta organização"
}
404Não encontrada

Ligação inexistente, sem gravação ou sem uniqueid.

{
  "error": "Ligação não encontrada ou não possui uniqueid"
}
429Limite atingido

Limite diário de transcrição da organização atingido (code transcription_daily_limit_reached; o consumo zera à meia-noite de Brasília e o header Retry-After diz em quantos segundos), ou a mesma ligação já foi transcrita o máximo de vezes em 24 h. Não repita o pedido antes do Retry-After.

{
  "error": "Limite de transcrição diário atingido. O consumo com o painel da BemMelhor excedeu o diário permitido. Para aumentar, entre em contato com o suporte: (32) 3211-0536.",
  "code": "transcription_daily_limit_reached",
  "retryAfterSeconds": 25200
}
500Falha ao transcrever

Erro na geração. Quando é falta de crédito no serviço de transcrição, a mensagem diz isso.

{
  "error": "Erro ao transcrever áudio (openai): Sem créditos na conta OpenAI (credit_balance_exhausted)."
}

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"
GET/calls/:callId/hangup-bycalls:get

Ver quem encerrou

Diz quem desligou a ligação, direto do registro da central: client quando foi o telefone físico e agent quando foi o ramal. Funciona com chave SK- (scope calls:get) ou token partner PT-. Use o linkedId, que é o campo Linkedid do webhook. Responde 404 quando a ligação não tem registro de encerramento. O mesmo valor chega sozinho no webhook de Hangup, no campo hangup_by_role.

Parâmetros

NomeLocalTipoDescrição
callIdobrigatóriopathstringIdentificador da ligação. Prefira o linkedId (Linkedid do webhook), que é o uniqueid do A-leg. O uniqueId de outra perna também funciona: a API traduz para o id da ligação antes de consultar a central.

Exemplo de requisição

# Ver quem encerrou
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/calls/1788981339.327/hangup-by" \
  -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"
GET/missed-callsmissed_calls:read

Chamadas perdidas

Chamadas que tocaram num ramal e não foram atendidas por ele, mais recentes primeiro. Com extension, só daquele ramal; sem extension, de todos os ramais da organização (cada item traz o seu). outcome missed é perdida de verdade e entra no unseenCount; answered_elsewhere foi atendida por outro ramal e aparece só no histórico. Funciona com chave SK- (scope) ou token partner PT-. Registros ficam 90 dias.

Parâmetros

NomeLocalTipoDescrição
extensionquerystringRamal. Sem ele, lista todos os ramais da organização.
onlyUnseenquerybooleantrue traz só as perdidas ainda não vistas (outcome missed e sem seenAt).
limitquerynumberQuantidade de registros, de 1 a 200 (padrão 50).

Query: marque e preencha para incluir no exemplo

Respostas

200OK

Lista e contador de não vistas (do ramal pedido, ou da organização sem extension).

{
  "extensionNumber": "6010",
  "items": [
    {
      "id": "b6d0f2f0-6d5a-4f3d-9d0e-6f1a2b3c4d5e",
      "extensionNumber": "6010",
      "linkedId": "1789496313.941",
      "uniqueId": "1789496317.942",
      "callerNumber": "5514996734602",
      "callerName": null,
      "extensionName": "Recepção",
      "extensionAsteriskName": "Thiago",
      "did": "5514981927913",
      "queue": "action_queue_atendimento",
      "outcome": "missed",
      "answeredBy": null,
      "ringingAt": "2026-10-20T14:10:29.448Z",
      "seenAt": null
    }
  ],
  "unseenCount": 1
}
400Parâmetros inválidos

limit fora de 1 a 200 ou onlyUnseen diferente de true/false.

{
  "error": "Parâmetros inválidos: use extension, onlyUnseen (true/false) e limit (1 a 200)"
}
403Sem permissão

A chave não possui o escopo missed_calls:read.

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

Exemplo de requisição

# Chamadas perdidas
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/missed-calls?limit=50" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/missed-calls/seenmissed_calls:write

Marcar chamadas perdidas como vistas

Marca como vistas as perdidas de um ramal. O visto é por ramal: zera o contador também no painel e no softphone de quem usa aquele ramal (e eles recebem o evento MissedCallSeen). Sem ids, marca todas as não vistas do ramal. Escopo separado de propósito: só libere se o seu sistema deve dar baixa no lugar do atendente.

Parâmetros

NomeLocalTipoDescrição
extensionobrigatóriobodystringRamal cujas perdidas serão marcadas.
idsbodystring[]Ids das perdidas (até 200). Sem ids, marca todas as não vistas do ramal.

Corpo de exemplo

{
  "extension": "6010",
  "ids": [
    "b6d0f2f0-6d5a-4f3d-9d0e-6f1a2b3c4d5e"
  ]
}

Respostas

200OK

Quantas foram marcadas agora e quantas ainda faltam ver no ramal.

{
  "extensionNumber": "6010",
  "seen": 1,
  "unseenCount": 0
}
400Dados inválidos

extension ausente ou ids que não são UUID.

{
  "error": "Informe extension: o visto é por ramal"
}
403Sem permissão

A chave não possui o escopo missed_calls:write.

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

Exemplo de requisição

# Marcar chamadas perdidas como vistas
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/missed-calls/seen" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/queue/pausequeues:pause

Pausar ramal na fila

Pausa um ramal em uma fila específica, igual à página de monitoramento. O motivo (reason) é obrigatório e fica no histórico de pausas. Não exige Telefonia & WhatsApp — apenas Asterisk configurado. O ramal precisa ser membro da fila.

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatóriobodystringNúmero do ramal
queueobrigatóriobodystringNome da fila
reasonobrigatóriobodystringMotivo da pausa (visível no monitoramento e no histórico)

Corpo de exemplo

{
  "extensionNumber": "6001",
  "queue": "vendas",
  "reason": "Almoço"
}

Respostas

200OK

Ramal pausado. O motivo fica registrado no histórico.

{
  "message": "Ramal pausado com sucesso",
  "pauseLog": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "extensionNumber": "6001",
    "queue": "vendas",
    "reason": "Almoço",
    "startedAt": "2026-08-31T17:00:00.000Z"
  }
}
400Dados inválidos

Motivo ausente, ramal já pausado nesta fila, ou body inválido.

{
  "error": "Motivo da pausa é obrigatório"
}
403Sem permissão

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

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

Ramal não é membro da fila, ou Asterisk não configurado na organização.

{
  "error": "Ramal 6001 não encontrado na fila vendas"
}

Exemplo de requisição

# Pausar ramal na fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/queue/pause" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/queue/unpausequeues:pause

Despausar ramal na fila

Retira a pausa do ramal na fila informada. Não envia motivo. Fecha o registro de pausa ativo (inclusive pausas feitas pelo painel ou por outro token).

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatóriobodystringNúmero do ramal
queueobrigatóriobodystringNome da fila

Corpo de exemplo

{
  "extensionNumber": "6001",
  "queue": "vendas"
}

Respostas

200OK

Ramal despausado.

{
  "message": "Ramal despausado com sucesso",
  "pauseLog": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "extensionNumber": "6001",
    "queue": "vendas",
    "reason": "Almoço",
    "startedAt": "2026-08-31T17:00:00.000Z",
    "endedAt": "2026-08-31T18:00:00.000Z"
  }
}
400Dados inválidos

Ramal não está pausado nesta fila, ou body inválido.

{
  "error": "Ramal 6001 não está pausado na fila vendas"
}
403Sem permissão

A chave não possui o escopo queues:pause.

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

Ramal não é membro da fila, ou Asterisk não configurado.

{
  "error": "Ramal 6001 não encontrado na fila vendas"
}

Exemplo de requisição

# Despausar ramal na fila
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/queue/unpause" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/extension/pausequeues:pause

Ativar DND do ramal

Coloca o ramal inteiro em DND (ocupado para todas as filas e ligações diretas), via Bridge. Use no softphone interno e em integrações. O motivo é obrigatório e fica no histórico. Exige Telefonia & WhatsApp / Bridge — diferente de /queue/pause, que é só por fila no Asterisk.

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatóriobodystringNúmero do ramal
reasonobrigatóriobodystringMotivo da pausa (visível no monitoramento e no histórico)

Corpo de exemplo

{
  "extensionNumber": "6001",
  "reason": "Almoço"
}

Respostas

201Created

DND ativado. O motivo fica registrado no histórico.

{
  "message": "Ramal colocado em DND com sucesso",
  "paused": true,
  "asteriskOk": true,
  "pauseLog": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "extensionNumber": "6001",
    "reason": "Almoço",
    "startedAt": "2026-09-04T17:00:00.000Z"
  }
}
400Dados inválidos

Motivo ausente, ramal já em DND, body inválido ou Bridge incompleta.

{
  "error": "Motivo da pausa é obrigatório"
}
403Sem permissão

A chave não possui o escopo queues:pause.

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

Ramal não existe no tenant Bridge.

{
  "error": "Ramal 9999 nao encontrado"
}

Exemplo de requisição

# Ativar DND do ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/extension/pause" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/extension/unpausequeues:pause

Remover DND do ramal

Remove o DND do ramal. Não envia motivo. Fecha o registro ativo (inclusive DND feito pelo painel ou por outro token).

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatóriobodystringNúmero do ramal

Corpo de exemplo

{
  "extensionNumber": "6001"
}

Respostas

200OK

DND removido.

{
  "message": "DND do ramal removido com sucesso",
  "paused": false,
  "asteriskOk": true,
  "durationSeconds": 300,
  "pauseLog": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "extensionNumber": "6001",
    "reason": "Almoço",
    "startedAt": "2026-09-04T17:00:00.000Z",
    "endedAt": "2026-09-04T17:05:00.000Z"
  }
}
400Dados inválidos

Ramal não está em DND, ou body inválido.

{
  "error": "Ramal 6001 nao esta pausado"
}
403Sem permissão

A chave não possui o escopo queues:pause.

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

Ramal não existe no tenant Bridge.

{
  "error": "Ramal 9999 nao encontrado"
}

Exemplo de requisição

# Remover DND do ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/extension/unpause" \
  -H "x-api-key: SUA_CHAVE_DE_API"
GET/extension/pausequeues:pause

Consultar DND do ramal

Retorna se o ramal está em DND, duração atual, motivo e o log local ativo (se houver). Use no softphone para pintar o botão de pausa.

Parâmetros

NomeLocalTipoDescrição
extensionNumberobrigatórioquerystringNúmero do ramal

Query: marque e preencha para incluir no exemplo

Respostas

200OK

Estado atual do DND.

{
  "paused": true,
  "extensionNumber": "6001",
  "durationSeconds": 120,
  "reason": "Almoço",
  "pausedAt": "2026-09-04T17:00:00.000Z",
  "pauseLog": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "extensionNumber": "6001",
    "reason": "Almoço",
    "asteriskOk": true,
    "startedAt": "2026-09-04T17:00:00.000Z",
    "endedAt": null
  }
}
400Dados inválidos

extensionNumber ausente ou Bridge incompleta.

{
  "error": "Número do ramal é obrigatório"
}
404Não encontrado

Ramal não existe no tenant Bridge.

{
  "error": "Ramal 9999 nao encontrado"
}

Exemplo de requisição

# Consultar DND do ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/extension/pause" \
  -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"
GET/telephony-bridge/extensions/:ext/pausetelephony_bridge:extensions:read

Consultar DND do ramal

Retorna se o ramal está em DND (ocupado globalmente). Mesmo histórico do GET /extension/pause.

Parâmetros

NomeLocalTipoDescrição
extobrigatóriopathstringNúmero do ramal

Exemplo de requisição

# Consultar DND do ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X GET "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions/6001/pause" \
  -H "x-api-key: SUA_CHAVE_DE_API"
POST/telephony-bridge/extensions/:ext/pausetelephony_bridge:extensions:write

Ativar DND do ramal

Ativa DND do ramal inteiro via Bridge. Motivo obrigatório. O softphone interno do dashboard deve preferir POST /organizations/:id/extension/pause (sessão JWT).

Parâmetros

NomeLocalTipoDescrição
extobrigatóriopathstringNúmero do ramal
reasonobrigatóriobodystringMotivo da pausa

Corpo de exemplo

{
  "reason": "Almoço"
}

Exemplo de requisição

# Ativar DND do ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X POST "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions/6001/pause" \
  -H "x-api-key: SUA_CHAVE_DE_API"
DELETE/telephony-bridge/extensions/:ext/pausetelephony_bridge:extensions:write

Remover DND do ramal

Remove o DND do ramal. Equivale a POST /extension/unpause.

Parâmetros

NomeLocalTipoDescrição
extobrigatóriopathstringNúmero do ramal

Exemplo de requisição

# Remover DND do ramal
# Parâmetros de path/query já preenchidos com exemplos.
curl -X DELETE "https://service.bemmelhor.com.br/api/integrations/v1/telephony-bridge/extensions/6001/pause" \
  -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. `timeout` é quanto cada agente toca por ciclo e `max_wait` é o teto de espera do cliente na fila (0 = sem limite). Os dois são inteiros de 0 a 86400 segundos.

Corpo de exemplo

{
  "name": "suporte",
  "display_name": "Suporte",
  "strategy": "ringall",
  "timeout": 20,
  "max_wait": 120
}

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","timeout":20,"max_wait":120}'
PATCH/telephony-bridge/queues/:nametelephony_bridge:queues:write

Atualizar fila

Atualiza a configuração de uma fila; só o que for enviado muda. Tempos: `timeout` (segundos que cada agente toca por ciclo) e `max_wait` (teto de espera do cliente na fila, 0 = sem limite), inteiros de 0 a 86400. A central grava, regenera a configuração das filas e recarrega sozinha.

Parâmetros

NomeLocalTipoDescrição
nameobrigatóriopathstringNome da fila

Corpo de exemplo

{
  "display_name": "Suporte comercial",
  "timeout": 20,
  "max_wait": 120
}

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","timeout":20,"max_wait":120}'
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 eventos em tempo real (fluxo da chamada, presença, WhatsApp, transferência e alertas). Assine apenas os eventos documentados; 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 segundos 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');
});

Webhook por número (WhatsApp provisionado)

Em organização com números de WhatsApp oficial provisionados, cada número costuma ser uma conexão separada no seu sistema. Os eventos de ligação desses números trazem de qual número são, e cada webhook pode escutar só alguns números.

  • OrganizationNumber: Número da empresa na ligação (E.164 sem +). Vai em todos os eventos da ligação, do começo ao fim, inclusive depois da URA, e em IncomingCallAllExtensionsOffline, WhatsAppCallOriginated e WhatsAppCallPermissionRequested.
  • GupshupAppId: App Gupshup do número. Só vai quando a ligação passa pelo WhatsApp oficial; se o mesmo número for usado numa linha comum, a ligação por essa linha traz só o OrganizationNumber.
  • Organização sem número provisionado (PSTN, WhatsApp não oficial) não recebe esses campos, e presença de ramal e ligação interna também não.
  • Filtro por número: no painel (Segurança → Webhooks) ou na API, em phoneNumbers ao criar ou editar o webhook. Ausente ou [] = todos os números, que é o padrão; todo webhook existente continua assim.
  • Com números escolhidos, o webhook só recebe os eventos cujo OrganizationNumber está na lista. Eventos sem número (presença de ramal, ligação interna, ligação por outro número) não chegam a ele: mantenha também um webhook com todos os números se precisar deles.
  • Só números provisionados da organização são aceitos (com ou sem +). A listagem de webhooks devolve os números disponíveis em provisionedNumbers (API da organização) ou availablePhoneNumbers (API de parceiro).
POST /api/organizations/{organizationId}/webhooks
{
  "name": "Digisac — Loja",
  "url": "https://seu-sistema.com/webhooks/loja",
  "events": ["Dial", "BridgeLink", "BridgeUnlink", "Hangup"],
  "phoneNumbers": ["5514981927913"]
}
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",
  "ClickToCallId": "6d0f2f0f-6d5a-4f3d-9d0e-6f1a2b3c4d5e"
}

Campos importantes

  • CallerIDNum: Quem está ligando (ramal ou número externo).
  • ClickToCallId: Presente quando a ligação foi disparada por POST /click-to-call. É o mesmo id devolvido na resposta da discagem e acompanha todos os eventos da chamada (Dial, Bridge, Hangup), permitindo amarrar a ligação ao registro que a originou.
  • 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.
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": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "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_acme",
  "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": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "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_acme",
  "dial_string": "cliente_acme-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": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "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": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "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).
13

Chamada entrante sem ramais online (IncomingCallAllExtensionsOffline)

Disparado quando uma ligação entra no PABX e nenhum ramal visível da organização está registrado (online) no Asterisk. Se qualquer ramal estiver online, mesmo ocupado, o evento não é enviado. Online significa registro SIP no PABX, não presença de chat.

{
  "Event": "IncomingCallAllExtensionsOffline",
  "organizationId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "CallerIDNum": "11999998888",
  "CallerIDName": "João",
  "Did": "1133334444",
  "Uniqueid": "1773335384.1802",
  "Linkedid": "1773335384.1802",
  "Channel": "SIP/tronco-00000001",
  "Context": "from-trunk",
  "timestamp": "2026-08-19T17:00:00.000Z",
  "message": "Nenhum ramal online para atender a chamada",
  "delivered_via": "webhook"
}

Campos importantes

  • CallerIDNum: Número de quem está ligando (chamada entrante).
  • Did: DID/Exten da empresa que recebeu a chamada, quando disponível.
  • message: Texto fixo informando que nenhum ramal está online para atender.
  • Uniqueid / Linkedid: Identificadores AMI da ligação (dedupe por chamada).

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, rememberDeviceEnabled), organization e eligibility.

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

    Corpo JSON opcional: allowedOrigins[], showDialpad?, isActive?, rememberDeviceEnabled?. Desligar rememberDeviceEnabled esquece todos os navegadores lembrados na hora.

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. Com rememberDeviceEnabled ligado, também devolve rememberToken e rememberExpiresAt.

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

    Entrada sem senha do navegador já lembrado. Corpo: { "publicId", "rememberToken", "parentOrigin" }. Mesma resposta do embed-login, com rememberToken girado a cada uso: guarde sempre o valor novo. Qualquer recusa volta 401, e aí é só apagar o que estava guardado e pedir a senha.

  • 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

Pausa DND do ramal (softphone interno)

O softphone flutuante do dashboard deve usar DND do ramal inteiro, não a pausa por fila. O ramal fica ocupado para filas e ligações diretas. Exige Telefonia & WhatsApp / Bridge na organização.

POST /queue/pause exige fila e só tira o ramal daquela fila (AMI). POST /extension/pause não envia fila: é DND global via Bridge e grava histórico em extension_pause_logs.

Autenticação: Cookie de sessão do painel (mesmo api client do dashboard). Papel mínimo: member. O usuário só pausa ramais vinculados a ele (UserExtension).

  • POSThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/extension/pause

    Ativa DND. Body: { extensionNumber, reason }. Motivo obrigatório (máx. 500). Resposta 201.

  • POSThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/extension/unpause

    Remove DND. Body: { extensionNumber }. Sem motivo. Resposta 200.

  • GEThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/extension/pause?extensionNumber=6001

    Status atual (Bridge + log local). Query extensionNumber obrigatória. Papel viewer+.

  • GEThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/extension/pause-logs

    Histórico paginado. Query opcional: extensionNumber, userId, startDate, endDate, page, limit.

  • GEThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/extension/pause-stats

    Estatísticas de DND (ativas, finalizadas, duração).

POST https://service.bemmelhor.com.br/api/organizations/{organizationId}/extension/pause
Content-Type: application/json

{
  "extensionNumber": "6001",
  "reason": "Almoço"
}

Softphone interno (dashboard)

  • Use o ramal selecionado no FloatingSoftphonePanel (selectedExtension).
  • Peça o motivo num modal, igual à página de ramais/monitoramento.
  • Ao abrir o painel, chame GET .../extension/pause?extensionNumber= para pintar o botão (pausado / disponível).
  • Não use /queue/pause aqui: o softphone não tem contexto de fila e o DND precisa ocupar o ramal inteiro.
  • Partner/embed continuam usando /api/integrations/v1/extension/pause (scope queues:pause).

GET /organizations/:id/asterisk/call-monitoring passa a incluir members[].isDnd, members[].dndReason e dndPauses[] (ramais em DND, mesmo fora de fila).

Helpers em lib/asterisk.ts: pauseExtensionDnd(organizationId, { extensionNumber, reason }), unpauseExtensionDnd(organizationId, { extensionNumber }), getExtensionDnd(organizationId, extensionNumber), getExtensionPauseLogs(organizationId, params?).

Chamadas perdidas do ramal

Ligação que toca num ramal e não é atendida por ele fica registrada como chamada perdida. Se outro ramal atendeu, a ocorrência é guardada como atendida em outro ramal e não entra na contagem. Vale para toda organização com AMI configurado, sem precisar ligar nada.

Como a ligação é classificada

  • Uma ocorrência por ligação e ramal: a fila pode tocar o mesmo ramal várias vezes e continua sendo um registro só.
  • O ramal atendeu: nada é registrado. A prova é o canal entrar em ponte (BridgeLink) ou passar para Up (Newstate).
  • Outro ramal atendeu: desfecho answered_elsewhere, visível na lista e fora do contador. O sinal é o Hangup com Cause 26.
  • Ninguém atendeu: desfecho missed, que é o que alimenta o contador.
  • Ficam de fora ligação de saída, perna de tronco e chamada sem número de fora (ramal para ramal).
  • O visto é por ramal: quem marca zera o contador para todos os usuários daquele ramal.
  • callerNumber é o número de quem ligou; callerName só vem quando a central manda o nome de quem ligou. extensionName é o nome do ramal (o cadastrado no painel ou, sem ele, o da central) e extensionAsteriskName é o nome do ramal na central.
  • Os registros são apagados depois de 90 dias.

Autenticação: No painel, cookie de sessão, papel mínimo member, e só ramal vinculado ao usuário (UserExtension). No iframe do softphone, Bearer do token do embed, e o ramal vem do próprio token. Para sistemas integrados, use a API de integração com chave SK- ou token PT-: GET /api/integrations/v1/missed-calls (escopo missed_calls:read) e POST /api/integrations/v1/missed-calls/seen (escopo missed_calls:write); a chave enxerga todos os ramais da organização e o ramal vai em extension.

Endpoints

  • GEThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/missed-calls?extension=6010&onlyUnseen=true&limit=50

    Lista as perdidas do ramal, mais recentes primeiro, com unseenCount. Sem extension, usa o primeiro ramal do usuário. onlyUnseen filtra o que falta ver; limit vai até 200 (padrão 50).

  • POSThttps://service.bemmelhor.com.br/api/organizations/{organizationId}/missed-calls/seen

    Marca como vistas. Body opcional: { extension, ids }. Sem ids, marca todas as não vistas do ramal. Responde { seen, unseenCount }.

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

    Mesma lista, para o iframe. Bearer do embed; o ramal vem do token e a query não escolhe outro.

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

    Marca como vistas no iframe. Body opcional: { ids }.

  • GEThttps://service.bemmelhor.com.br/api/integrations/v1/missed-calls?extension=6010&onlyUnseen=true&limit=50

    API de integração (SK- ou PT-, escopo missed_calls:read). Mesma lista; sem extension, traz todos os ramais da organização e o unseenCount da organização.

  • POSThttps://service.bemmelhor.com.br/api/integrations/v1/missed-calls/seen

    API de integração (escopo missed_calls:write). Body: { extension, ids? }; extension é obrigatório porque o visto é por ramal.

GET https://service.bemmelhor.com.br/api/organizations/{organizationId}/missed-calls?extension=6010&onlyUnseen=true&limit=50

{
  "extensionNumber": "6010",
  "items": [
    {
      "id": "b6d0f2f0-6d5a-4f3d-9d0e-6f1a2b3c4d5e",
      "extensionNumber": "6010",
      "linkedId": "1789496313.941",
      "uniqueId": "1789496317.942",
      "callerNumber": "5514996734602",
      "callerName": null,
      "extensionName": "Recepção",
      "extensionAsteriskName": "Thiago",
      "did": null,
      "queue": "action_queue_digisac",
      "outcome": "missed",
      "answeredBy": null,
      "ringingAt": "2026-09-17T14:10:29.448Z",
      "seenAt": null
    }
  ],
  "unseenCount": 1
}

Tempo real (WSS)

wss://service.bemmelhor.com.br/ws/telephony-events

Autenticação: Mesma conexão dos eventos de transferência. Token do softphone do parceiro, token do iframe (os dois já trazem o ramal) ou token de WebSocket da sessão do painel, e aí organizationId na query é obrigatório e os ramais saem dos vínculos do usuário.

  • A chave de integração SK- não abre este WebSocket (a conexão fecha com 1008 "Token inválido ou expirado"). Sistema integrado consulta pela API de integração, em GET /api/integrations/v1/missed-calls.
  • Eventos de ramal só chegam para conexão que tem ramal. O connected traz extensions com os ramais daquela conexão.
  • Os eventos de transferência continuam chegando por organização, como antes.
  • MissedCall

    Ligação perdida naquele ramal. O payload traz o registro inteiro mais unseenCount, que é o número do contador.

  • MissedCallSeen

    As perdidas do ramal foram vistas em alguma tela. Serve para zerar o contador nas outras abas.

Consultas pela própria conexão

// envia
{ "type": "missed_calls:list", "onlyUnseen": false, "limit": 50 }
// recebe
{ "type": "missed_calls", "extensionNumber": "6010", "items": [...], "unseenCount": 1 }

// envia
{ "type": "missed_calls:seen" }
// recebe
{ "type": "missed_calls:seen", "extensionNumber": "6010", "seen": 1, "unseenCount": 0 }

// envia
{ "type": "ping" }
// recebe
{ "type": "pong", "ts": 1789496317942 }

Na tela do atendente

  • O softphone tem duas abas: “Telefone” e “Perdidas”. A aba Perdidas só aparece com o ramal logado (registrado na central) e mostra a lista daquele ramal, com o contador de não vistas.
  • Abrir a aba conta como ver: as não vistas são marcadas e o contador zera em todas as telas daquele ramal.
  • Com o softphone fechado, o botão flutuante mostra uma marca vermelha com a contagem, mesmo sem o ramal logado: aí o contador vem do primeiro ramal vinculado ao usuário, consultado a cada minuto.
  • Chamada em andamento manda no painel: a aba volta para Telefone e a barra de abas fica oculta até o fim da ligação.
  • Helpers: lib/missedCalls.ts (consulta e visto, nas duas portas), lib/telephonyEvents.ts (conexão WSS com reconexão) e o hook app/components/softphone/useMissedCalls.ts.

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 (qualquer ramal da organização, vinculado ou não a usuário). 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 *).
  • Pausar/despausar ramal em fila (motivo obrigatório, igual ao monitoramento): POST /api/integrations/v1/queue/pause e /queue/unpause (scope queues:pause; PT já cobre com *).
  • DND do ramal inteiro (ocupado em todas as filas e ligações diretas, via Bridge): POST /api/integrations/v1/extension/pause { extensionNumber, reason }, POST /extension/unpause { extensionNumber }, GET /extension/pause?extensionNumber=6001 (scope queues:pause). O softphone interno do dashboard usa as rotas de sessão /api/organizations/:id/extension/pause.
  • 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.
  • Antes de ligar no WhatsApp oficial, GET telephony-bridge/calls/permission?to_msisdn= diz se o cliente autorizou (allowed). A consulta não envia pedido ao cliente.

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, pausa de ramal em fila, DND do ramal 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"
}
404Sessão para ramal que não existe no tenant da organização
{
  "error": "Ramal não encontrado nesta organização"
}
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 e, se quiser validar antes, GET telephony-bridge/calls/permission. 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.

Consultar permissão antes de ligar (WhatsApp oficial)

GET …/telephony-bridge/calls/permission diz se o número pode receber ligação oficial agora. allowed: true só com status: accepted dentro da validade. A consulta não envia pedido ao cliente: o pedido continua saindo no POST de ligação.

NomeLocalTipoDescrição
to_msisdnreqquerystringDestino só com dígitos, com código do país e DDD. O 9 de celular é opcional: os dois formatos são consultados.
from_didquerystringDID de origem. Se omitido, usa o primeiro DID da org (via capabilities).
  • allowed boolean · true quando a ligação oficial pode ser feita agora (status accepted e dentro da validade).
  • status string · accepted (autorizou), pending (pedido enviado, sem resposta), rejected (recusou), expired (autorização vencida) ou not_requested (nunca foi pedida).
  • from_did / to_msisdn string · DID e número exatamente como a permissão está registrada (com ou sem o 9).
  • expires_at / requested_at string | null · Validade da autorização e data do pedido, em ISO 8601.
  • message string · Texto pronto para mostrar ao usuário.
curl "https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/telephony-bridge/calls/permission?to_msisdn=5511987654321&from_did=551140028922" \
  -H "Authorization: Bearer PT-ACME-seu_segredo"
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
extensionNumberopcionalbodystringRamal da organização: número curto (6001) ou nome com o tenant (cliente_acme-6001). Não precisa estar vinculado a usuário. Obrigatório se userExtensionId não for enviado.ex.: 6001
userExtensionIdopcionalbodyuuidID do vínculo usuário-ramal, para quem prefere apontar o vínculo. Só vale para ramal vinculado; para os demais use extensionNumber.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) para qualquer ramal da organização, vinculado ou não a um usuário. Informe extensionNumber ou userExtensionId. Não devolve senha SIP.

    • • O ramal não precisa estar vinculado a um usuário no painel: o vínculo é para uso interno (painel e embed).
    • • extensionNumber aceita o número curto (6001) ou o nome com o tenant (cliente_acme-6001).
    • • Ramal que não existe no tenant da organização responde 404.
    {
      "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"
    }
  • GETConsultar permissão WhatsApp200
    https://service.bemmelhor.com.br/api/partner/v1/organizations/{organizationId}/telephony-bridge/calls/permission?to_msisdn=5511987654321

    Diz se o número pode receber ligação WhatsApp oficial (call_type=1) antes de discar. Só leitura: não envia pedido de permissão ao cliente.

    • • allowed=true só com status=accepted dentro da validade (expires_at).
    • • status: accepted, pending, rejected, expired ou not_requested.
    • • Query from_did opcional: sem ela, usa o primeiro DID da org.
    • • Consulta o número com e sem o 9º dígito do celular.
    {
      "allowed": true,
      "status": "accepted",
      "from_did": "551140028922",
      "to_msisdn": "5511987654321",
      "expires_at": "2026-12-28T14:02:11.000Z",
      "requested_at": "2026-09-28T14:01:03.000Z",
      "message": "O cliente autorizou receber ligações pelo WhatsApp."
    }
  • 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.