npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@growhats/webphone

v1.3.18

Published

Webphone widget para chamadas VoIP WhatsApp via GroWHats — drop-in replacement para @wavoip/wavoip-webphone

Readme

@growhats/webphone

Biblioteca para realizar chamadas VoIP WhatsApp via GroWHats. Disponibiliza uma interface customizavel e isolada do projeto onde esta instalada, com gravacao full-duplex automatica e persistencia contra fechamento de pagina.

Drop-in replacement para @wavoip/wavoip-webphone — API 100% compativel.

Modos de uso

O SDK pode ser usado de duas formas:

1. Widget completo (padrao)

Botao flutuante com discador, modal de chamada recebida e painel de chamada ativa. Tudo pronto, sem codigo extra:

await GrowhatsWebphone.render({
  serverUrl: 'https://sua-api.com',
  accountId: 1,
  inboxId: 1,
  apiKey: 'TOKEN',
  recording: { enabled: true },
});

2. Modo discreto (apenas chamadas recebidas)

Sem botao flutuante — so aparece notificacao centralizada quando chega chamada. Permite que voce construa seu proprio discador e use as funcoes do SDK para fazer chamadas. Ideal pra integrar no seu CRM/ERP existente:

const api = await GrowhatsWebphone.render({
  serverUrl: 'https://sua-api.com',
  accountId: 1,
  inboxId: 1,
  apiKey: 'TOKEN',
  recording: { enabled: true },
  showWidgetButton: false,  // esconde o FAB
});

// Seu botao customizado para ligar:
document.getElementById('meu-botao-ligar').onclick = async () => {
  const numero = document.getElementById('meu-input').value;
  await api.call.start(numero);  // widget cuida do resto (UI, audio, gravacao)
};

Notificacao de chamada recebida: aparece centralizada no topo da tela, sem bloquear cliques no resto da pagina. Usuario pode:

  • Atender — abre painel de chamada ativa
  • Recusar — fecha notificacao
  • Minimizar — vira uma pill flutuante pulsante no canto superior direito, com telefone + timer

Atendentes (multi-agente na mesma inbox)

Varios usuarios do seu CRM podem estar conectados ao mesmo numero WhatsApp. O SDK coordena isso para voce: basta passar o agent no render().

const api = await GrowhatsWebphone.render({
  serverUrl: 'https://api.empresa.com',
  accountId: 1,
  inboxId: 81,
  apiKey,
  agent: {
    idExternal: 'user-123',           // obrigatorio: ID do usuario no SEU sistema
    name: 'Joao Silva',
    email: '[email protected]',
    role: 'sales',
    metadata: { team: 'BR-NE' },      // campo livre
  },
});

O que o SDK faz com isso

  • Atribuicao — cada CallLog salva agentIdExternal (quem originou) e answeredByAgentId (quem atendeu). Chamadas outbound, aceites inbound e gravacoes carregam essa info.
  • Primeiro-a-atender ganha — quando a mesma inbox e compartilhada, o primeiro accept() reivindica a chamada. Os outros webphones recebem call:taken e o modal some sozinho. Se o seu accept() perdeu a corrida, voce recebe call:claim:denied com quem ganhou.
  • Presencaagent:joined / agent:left disparam quando outro atendente conecta/desconecta na mesma inbox. Use api.agent.getOnlineAgents() para listar quem esta online.
  • Gravacoes — sao gravadas em call-recordings/{accountId}/{AAAA}/{MM}/{DD}/{agentIdExternal}/{callId}.webm, facilitando auditoria por atendente.

API

api.agent.get();                 // AgentInfo | null — agente ativo
api.agent.set({ idExternal: 'user-456', name: 'Maria' });  // troca em tempo real
api.agent.set(null);             // desativa atribuicao
await api.agent.getOnlineAgents(); // lista atendentes online na inbox

Eventos multi-agente

api.on('call:taken', ({ callId, takenBy }) => {
  // Outro atendente atendeu. Atualiza seu CRM — o widget ja fechou sozinho.
  console.log(`${takenBy.name || takenBy.idExternal} atendeu ${callId}`);
});

api.on('call:claim:denied', ({ takenBy }) => {
  // Voce tentou atender mas perdeu a corrida.
  alert(`Ja atendido por ${takenBy.name || takenBy.idExternal}`);
});

api.on('agent:joined', (a) => console.log('entrou:', a.name));
api.on('agent:left',   (a) => console.log('saiu:',   a.name));

Quando agent e opcional

Se voce nao passar agent, tudo continua funcionando igual — sem atribuicao, sem coordenacao entre peers, gravacao sob agent-anonymous/. Use quando houver apenas um atendente por inbox ou nao precisar identificar quem atendeu.

API para construir seu proprio discador

Quando showWidgetButton: false, voce controla as chamadas via API:

Iniciar chamada (outgoing)

const call = await api.call.start('5511999999999');
// Retorna CallOutgoing — widget mostra painel de chamada automaticamente

Encerrar chamada

// Pegar chamada ativa (atendida)
const active = api.call.getCallActive();
if (active) await active.end();

// Pegar chamada de saida (ainda chamando)
const outgoing = api.call.getCallOutgoing();
if (outgoing) await outgoing.end();

Mutar/Desmutar

const call = api.call.getCallActive();
if (call) {
  await call.mute();    // muta
  await call.unmute();  // desmuta
  call.muted;           // true/false
}

Escutar eventos

api.on('call:incoming', (offer) => {
  console.log('Chamada recebida de:', offer.from);
  // O widget ja mostra a notificacao automaticamente.
  // Mas voce pode fazer logica extra aqui (tocar seu proprio som, abrir outra tela, etc)
});

api.on('call:started', (call) => {
  console.log('Chamada iniciada:', call.id, call.direction, call.peer.phone);
});

api.on('call:ended', (callId, reason) => {
  console.log('Chamada encerrada:', callId, reason);
});

api.on('recording:started', (callId) => {
  console.log('Gravacao iniciada:', callId);
});

api.on('recording:stopped', (callId, result) => {
  if (result) {
    console.log('Gravacao salva:', result.url, result.durationSeconds + 's');
  }
});

api.on('connection:status', (connected) => {
  console.log('Servidor:', connected ? 'online' : 'offline');
});

api.on('error', (err) => {
  console.error('Erro:', err.message);
});

Atender/Recusar programaticamente

Normalmente o widget ja mostra os botoes, mas se quiser controle total:

api.on('call:incoming', async (offer) => {
  // Sua logica customizada
  const shouldAccept = window.confirm(`Aceitar chamada de ${offer.from}?`);
  if (shouldAccept) {
    await offer.accept();  // audio conecta automaticamente
  } else {
    await offer.reject();
  }
});

Buscar gravacao depois

const recordingUrl = await api.recording.getUrl(callId);
// Retorna URL (relativa) onde pode baixar/reproduzir o audio .webm
// Exemplo: /api/v1/accounts/1/calls/abc123/recording

Destruir o SDK

GrowhatsWebphone.destroy();
// Limpa widget, socket, audio, gravacoes pendentes

Instalacao

# pnpm
pnpm add @growhats/webphone

# npm
npm install @growhats/webphone

# yarn
yarn add @growhats/webphone

Via CDN:

<script src="https://cdn.jsdelivr.net/npm/@growhats/webphone@latest/dist/growhats-webphone.umd.js"></script>

Getting Started

Pacote instalado:

import { GrowhatsWebphone } from "@growhats/webphone";

const api = await GrowhatsWebphone.render({
  serverUrl: "https://sua-api.com",
  accountId: 1,
  inboxId: 81,
  apiKey: "sua-api-key",
});

GrowhatsWebphone.destroy(); // remover widget

Via CDN:

const api = await GrowhatsWebphone.render({
  serverUrl: "https://sua-api.com",
  accountId: 1,
  inboxId: 81,
  apiKey: "sua-api-key",
});

// Tambem acessivel via window
window.growhatsWebphone.render(config);
window.wavoip.call.start("5511999999999"); // compatibilidade Wavoip

Importante: Sempre use await ao chamar render() para evitar comportamento inesperado.

WebphoneAPI

render() retorna uma Promise<WebphoneAPI> e tambem expoe window.wavoip e window.growhatsWebphone:

type WebphoneAPI = {
  call: CallAPI;
  device: DeviceAPI;
  notifications: NotificationsAPI;
  widget: WidgetAPI;
  theme: ThemeAPI;
  position: PositionAPI;
  settings: SettingsAPI;
  recording: RecordingAPI;
  on<K extends keyof WebphoneEvents>(event: K, cb: WebphoneEvents[K]): void;
  off<K extends keyof WebphoneEvents>(event: K, cb: WebphoneEvents[K]): void;
  destroy(): void;
};

CallAPI

type CallAPI = {
  start(
    to: string,
    config?: { fromTokens?: string[]; displayName?: string }
  ): Promise<CallOutgoing>;

  startCall(to: string, fromTokens?: string[]): Promise<CallOutgoing>; // Deprecated

  getCallActive(): CallActive | undefined;

  getCallOutgoing(): CallOutgoing | undefined;

  getOffers(): CallOffer[];

  setInput(to: string): void;

  onOffer(cb: (offer: CallOffer) => void): void;
};

CallOffer

Representa uma chamada recebida aguardando acao:

type CallOffer = {
  call_id: string;
  from: string;
  displayName?: string;

  accept(): Promise<CallActive>;
  reject(): Promise<{ err: string | null }>;

  onAcceptedElsewhere(cb: () => void): void;
  onRejectedElsewhere(cb: () => void): void;
  onUnanswered(cb: () => void): void;
  onEnd(cb: () => void): void;
  onStatus(cb: (status: CallStatus) => void): void;
};

CallActive

Chamada em andamento (atendida):

type CallActive = {
  id: string;
  type: "official" | "unofficial";
  device_token: string;
  direction: "INCOMING" | "OUTGOING";
  status: CallStatus;
  connection_status: "disconnected" | "connected" | "connecting";
  muted: boolean;
  peer: {
    phone: string;
    displayName: string | null;
    profilePicture: string | null;
    muted: boolean;
  };

  mute(): Promise<{ err: string | null }>;
  unmute(): Promise<{ err: string | null }>;
  end(): Promise<{ err: string | null }>;

  onError(cb: (err: string) => void): void;
  onPeerMute(cb: () => void): void;
  onPeerUnmute(cb: () => void): void;
  onEnd(cb: () => void): void;
  onStats(cb: (stats: CallStats) => void): void;
  onConnectionStatus(cb: (status: string) => void): void;
  onStatus(cb: (status: CallStatus) => void): void;
};

CallOutgoing

Chamada de saida (aguardando atendimento):

type CallOutgoing = {
  id: string;
  type: "official" | "unofficial";
  device_token: string;
  direction: "OUTGOING";
  status: CallStatus;
  muted: boolean;
  peer: {
    phone: string;
    displayName: string | null;
    profilePicture: string | null;
    muted: boolean;
  };

  mute(): Promise<{ err: string | null }>;
  unmute(): Promise<{ err: string | null }>;
  end(): Promise<{ err: string | null }>;

  onPeerAccept(cb: () => void): void;
  onPeerReject(cb: () => void): void;
  onUnanswered(cb: () => void): void;
  onEnd(cb: () => void): void;
  onStatus(cb: (status: CallStatus) => void): void;
};

CallStatus

type CallStatus =
  | "RINGING"
  | "CALLING"
  | "NOT_ANSWERED"
  | "ACTIVE"
  | "ENDED"
  | "REJECTED"
  | "FAILED"
  | "DISCONNECTED"
  | "DEVICE_RESTARTING";

CallStats

type CallStats = {
  rtt: {
    client: { min: number; max: number; avg: number };
    whatsapp: { min: number; max: number; avg: number };
  };
  tx: { total: number; total_bytes: number; loss: number };
  rx: { total: number; total_bytes: number; loss: number };
};

DeviceAPI

type DeviceAPI = {
  get(): Device[];
  getDevices(): Device[];                               // Deprecated

  add(token: string, persist?: boolean): void;
  addDevice(token: string, persist?: boolean): void;    // Deprecated

  remove(token: string): void;
  removeDevice(token: string): void;                    // Deprecated

  enable(token: string): void;
  enableDevice(token: string): void;                    // Deprecated

  disable(token: string): void;
  disableDevice(token: string): void;                   // Deprecated
};

persist: true salva o dispositivo no Local Storage do navegador entre sessoes. disable() encerra a conexao do webphone com o dispositivo enquanto o dispositivo continua rodando.

Device

type Device = {
  token: string;
  status: DeviceStatus;
  qrcode?: string;
  contact?: { phone?: string; name?: string };

  onStatus(cb: (status: DeviceStatus) => void): void;
  onQRCode(cb: (qr: string) => void): void;
  onContact(cb: (contact: { phone?: string; name?: string }) => void): void;
};

type DeviceStatus =
  | "disconnected"
  | "close"
  | "connecting"
  | "open"
  | "restarting"
  | "hibernating"
  | "BUILDING"
  | "EXTERNAL_INTEGRATION_ERROR";

NotificationsAPI

type NotificationsAPI = {
  get(): Notification[];
  getNotifications(): Notification[];                               // Deprecated

  add(notification: Partial<Notification>): void;
  addNotification(notification: Partial<Notification>): void;       // Deprecated

  remove(id: Date): void;
  removeNotification(id: Date): void;                               // Deprecated

  clear(): void;
  clearNotifications(): void;                                       // Deprecated

  read(): void;
  readNotifications(): void;                                        // Deprecated
};

type Notification = {
  id: Date;
  type: "INFO" | "CALL_FAILED";
  message: string;
  detail?: string;
  token?: string;
  isRead: boolean;
  isHidden: boolean;
  created_at: Date;
};

RecordingAPI

Gravacao automatica full-duplex (ambos os lados da conversa). Quando habilitada via config, grava automaticamente do momento que a chamada e atendida ate quando e encerrada. Suporta multiplas chamadas simultaneas isoladas.

Se o usuario fechar a pagina (F5 / fechar aba), a gravacao e salva automaticamente via navigator.sendBeacon.

type RecordingAPI = {
  /** Se a gravacao esta habilitada via config */
  enabled: boolean;

  /** Verifica se uma chamada especifica esta sendo gravada */
  isRecording(callId?: string): boolean;

  /** Retorna IDs de todas as chamadas sendo gravadas */
  getActiveRecordings(): string[];

  /** Busca a URL de uma gravacao finalizada no servidor */
  getUrl(callId: string): Promise<string | null>;
};

type RecordingResult = {
  url: string;
  callId: string;
  durationSeconds: number;
  startedAt: string;
  endedAt: string;
};

As gravacoes sao armazenadas no servidor organizadas por empresa e data:

call-recordings/{accountId}/{YYYY}/{MM}/{DD}/{callId}.webm

WidgetAPI

type WidgetAPI = {
  isOpen: boolean;

  open(): void;

  close(): void;

  toggle(): void;

  buttonPosition: {
    value: { x: number; y: number };
    set(
      position:
        | "top-left"
        | "top-right"
        | "bottom-left"
        | "bottom-right"
        | "center"
        | { x: number; y: number }
    ): void;
  };
};

ThemeAPI

type ThemeAPI = {
  value: "light" | "dark" | "system";
  set(theme: "light" | "dark" | "system"): void;
  setTheme(theme: "light" | "dark" | "system"): void;  // Deprecated
};

PositionAPI

type PositionAPI = {
  value: { x: number; y: number };
  set(
    position:
      | "top-left"
      | "top-right"
      | "bottom-left"
      | "bottom-right"
      | "center"
      | { x: number; y: number }
  ): void;
};

Configuracao Inicial (WebphoneConfig)

GrowhatsWebphone.render(config?: WebphoneConfig);

type WebphoneConfig = {
  /** URL base do servidor (obrigatorio) */
  serverUrl: string;

  /** ID da conta/empresa (obrigatorio) */
  accountId: number;

  /** ID do inbox WhatsApp para chamadas (obrigatorio) */
  inboxId: number;

  /** Chave de autenticacao (enviada como X-Api-Key) */
  apiKey?: string;

  /** Gravacao automatica de chamadas */
  recording?: {
    /** Habilitar gravacao automatica full-duplex (default: false) */
    enabled: boolean;
  };

  /** Tema: "light" | "dark" | "system" (default: "dark") */
  theme?: "light" | "dark" | "system";

  /** Posicao do botao flutuante (default: "bottom-right") */
  position?:
    | "top-left"
    | "top-right"
    | "bottom-left"
    | "bottom-right"
    | "center"
    | { x: number; y: number };

  /** Exibir botao flutuante do widget (default: true) */
  showWidgetButton?: boolean;

  /** Iniciar com o painel aberto (default: false) */
  startOpen?: boolean;

  /** Reconectar automaticamente em caso de queda (default: true) */
  autoReconnect?: boolean;

  /** Elemento container para montar o widget (default: document.body) */
  container?: HTMLElement;

  /** Configuracao da barra de status */
  statusBar?: {
    showNotificationsIcon?: boolean;
    showSettingsIcon?: boolean;
  };

  /** Configuracao do menu de configuracoes */
  settingsMenu?: {
    showDevices?: boolean;
    showAddDevices?: boolean;
    showEnableDevices?: boolean;
    showRemoveDevices?: boolean;
  };

  /**
   * Atendente que esta operando o webphone. Opcional — quando presente, chamadas sao
   * registradas com o agente, a gravacao e organizada por agente e varios
   * webphones no mesmo inbox se coordenam: o primeiro a atender ganha, os outros
   * recebem `call:taken` e o modal some sozinho. Veja a secao "Atendentes".
   */
  agent?: {
    idExternal: string;                       // obrigatorio: ID do atendente no seu sistema
    name?: string;
    email?: string;
    role?: string;                            // ex: "sales", "support"
    metadata?: Record<string, unknown>;
  };
};

SettingsAPI (Runtime)

Apos o render(), controle as configuracoes em tempo real via api.settings:

type SettingsAPI = {
  showNotifications: boolean;
  setShowNotifications(show: boolean): void;

  showSettings: boolean;
  setShowSettings(show: boolean): void;

  showDevices: boolean;
  setShowDevices(show: boolean): void;

  showAddDevices: boolean;
  setShowAddDevices(show: boolean): void;

  showEnableDevices: boolean;
  setShowEnableDevices(show: boolean): void;

  showRemoveDevices: boolean;
  setShowRemoveDevices(show: boolean): void;

  showWidgetButton: boolean;
  setShowWidgetButton(show: boolean): void;
};

Eventos

type WebphoneEvents = {
  /** Chamada recebida */
  "call:incoming": (offer: CallOffer) => void;

  /** Chamada iniciada (atendida ou discando) */
  "call:started": (call: CallActive) => void;

  /** Chamada encerrada */
  "call:ended": (callId: string, reason: string) => void;

  /** Status da chamada alterado */
  "call:status": (callId: string, status: CallStatus) => void;

  /** Outro atendente da mesma inbox atendeu a chamada (multi-agente) */
  "call:taken": (evt: { callId: string; takenBy: { idExternal: string; name?: string; email?: string; claimedAt: number } }) => void;

  /** Este cliente tentou atender, mas outro ja tinha reivindicado */
  "call:claim:denied": (evt: { callId: string; takenBy: { idExternal: string; name?: string; email?: string; claimedAt: number } }) => void;

  /** Um atendente conectou na mesma inbox */
  "agent:joined": (agent: { idExternal: string; name?: string; email?: string; role?: string; socketId: string; inboxId?: number; since: number }) => void;

  /** Um atendente desconectou da mesma inbox */
  "agent:left": (agent: { idExternal: string; name?: string; email?: string; role?: string; socketId: string; inboxId?: number; since: number }) => void;

  /** Status da conexao com servidor */
  "connection:status": (connected: boolean) => void;

  /** Gravacao iniciada automaticamente */
  "recording:started": (callId: string) => void;

  /** Gravacao finalizada e salva no servidor */
  "recording:stopped": (callId: string, result: RecordingResult | null) => void;

  /** Erro */
  error: (error: Error) => void;
};

Exemplo completo:

const api = await GrowhatsWebphone.render({
  serverUrl: "https://sua-api.com",
  accountId: 1,
  inboxId: 81,
  recording: { enabled: true },
});

api.on("connection:status", (connected) => {
  console.log("Conexao:", connected ? "online" : "offline");
});

api.on("call:incoming", (offer) => {
  console.log("Chamada recebida de:", offer.from);
  offer.accept(); // ou offer.reject()
});

api.on("call:started", (call) => {
  console.log("Em chamada:", call.peer.phone, call.direction);
});

api.on("call:ended", (callId, reason) => {
  console.log("Chamada encerrada:", callId, reason);
});

api.on("recording:started", (callId) => {
  console.log("Gravando chamada:", callId);
});

api.on("recording:stopped", (callId, result) => {
  if (result) {
    console.log("Gravacao salva:", result.url, result.durationSeconds + "s");
  }
});

api.on("error", (err) => {
  console.error("Erro:", err.message);
});

Compatibilidade com Wavoip

A API e 100% compativel com @wavoip/wavoip-webphone. O global window.wavoip e exposto automaticamente apos render():

// Todos os metodos da Wavoip funcionam:
window.wavoip.call.startCall("5511999999999", [tokenDispositivo]);
window.wavoip.device.addDevice(token);
window.wavoip.device.enableDevice(token);
window.wavoip.device.disableDevice(token);
window.wavoip.device.removeDevice(token);
window.wavoip.notifications.getNotifications();
window.wavoip.notifications.addNotification({ type: "INFO", message: "Teste" });
window.wavoip.settings.setShowWidgetButton(false);
window.wavoip.settings.setShowNotifications(false);
window.wavoip.settings.setShowSettings(false);
window.wavoip.theme.setTheme("dark");
window.wavoip.widget.toggle();

Guia de migracao

Para migrar de @wavoip/wavoip-webphone para @growhats/webphone:

  1. Substitua o import/script:
- import WavoipWebphone from "@wavoip/wavoip-webphone"
+ import { GrowhatsWebphone } from "@growhats/webphone"
- <script src="https://cdn.jsdelivr.net/npm/@wavoip/wavoip-webphone/dist/index.umd.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/@growhats/webphone/dist/growhats-webphone.umd.js"></script>
  1. Adicione os parametros obrigatorios no render():
- WavoipWebphone.render()
+ GrowhatsWebphone.render({
+   serverUrl: "https://sua-api.com",
+   accountId: 1,
+   inboxId: 81,
+ })
  1. Todo o restante (window.wavoip.*) continua funcionando sem alteracao.

Licenca

MIT