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

@z-api/call

v1.1.0

Published

Z-API Call SDK - WhatsApp VoIP call widget for web apps

Readme

@z-api/call

SDK para integrar chamadas VoIP do WhatsApp em aplicações web. Fornece um widget pronto (webphone) com discador, lista de chamadas e tela de ligação ativa, além de uma API programática completa.

Instalação

npm install @z-api/call

Via CDN (script tag)

<script src="https://unpkg.com/@z-api/call/dist/z-api-call.global.js"></script>
<script>
  const client = ZAPICall.init({
    instanceId: "SUA_INSTANCE_ID",
    getToken: async () => {
      const res = await fetch("https://seu-backend.com/call-token");
      const data = await res.json();
      return data.token;
    },
  });
</script>

Quick Start

import { init } from "@z-api/call";

const client = init({
  instanceId: "SUA_INSTANCE_ID",
  getToken: async () => {
    // Chama SEU backend, que por sua vez chama a Z-API para obter o token efêmero
    const res = await fetch("https://seu-backend.com/call-token");
    const data = await res.json();
    return data.token;
  },
});

// Ouvir eventos
client.on("call:incoming", (call) => {
  console.log("Chamada recebida de", call.from);
});

client.on("connected", () => {
  console.log("Conectado ao servidor");
});

Autenticação (getToken)

A SDK utiliza tokens efêmeros para autenticação. Em vez de passar um token fixo, você fornece uma função getToken que é chamada sempre que a SDK precisa se conectar (inclusive em reconexões).

Fluxo de autenticação

┌──────────┐      getToken()      ┌───────────────┐     GET /call-token     ┌─────────────┐
│   SDK    │ ──────────────────► │  Seu Backend  │ ──────────────────────► │   Z-API     │
│ (browser)│                      │               │                         │             │
│          │ ◄────────────────── │               │ ◄────────────────────── │             │
│          │    token efêmero     │               │     token efêmero       │             │
└──────────┘                      └───────────────┘                         └─────────────┘
  1. A SDK chama a função getToken fornecida por você
  2. Sua função faz uma requisição ao seu backend
  3. Seu backend chama a API da Z-API para gerar o token efêmero:
GET https://api.z-api.io/instances/{ID_INSTANCE}/token/{TOKEN_INSTANCE}/call-token
Header: client-token: SEU_CLIENT_TOKEN
  1. A Z-API retorna o token efêmero
  2. Seu backend repassa o token para o frontend
  3. A SDK usa o token para conectar via WebSocket

Exemplo de backend (Node.js)

app.get("/call-token", async (req, res) => {
  const response = await fetch(
    `https://api.z-api.io/instances/${ID_INSTANCE}/token/${TOKEN_INSTANCE}/call-token`,
    {
      method: "GET",
      headers: { "client-token": SEU_CLIENT_TOKEN },
    },
  );
  const data = await response.json();
  res.json({ token: data.token });
});

Exemplo de getToken no frontend

const client = init({
  instanceId: "SUA_INSTANCE_ID",
  getToken: async () => {
    const res = await fetch("https://seu-backend.com/call-token");
    const { token } = await res.json();
    return token;
  },
});

Importante: Nunca exponha o TOKEN_INSTANCE ou client-token no frontend. A chamada à Z-API deve ser feita pelo seu backend.

Chamada One-Shot (ZAPICall.call)

Para cenarios onde voce quer apenas um botao "Ligar" no seu sistema, sem gerenciar o ciclo de vida da SDK. Uma unica chamada faz tudo: conecta, abre o widget, liga, e destroi tudo ao encerrar.

import { ZAPICall } from "@z-api/call";

const handle = ZAPICall.call({
  instanceId: "SUA_INSTANCE_ID",
  getToken: async () => {
    const res = await fetch("https://seu-backend.com/call-token");
    const { token } = await res.json();
    return token;
  },
  number: "5511999999999",
});

O widget abre automaticamente em modo minimal (sem bolha flutuante, sem abas, apenas a tela de chamada) e pode ser arrastado para qualquer lugar da tela.

Controle da chamada (opcional)

handle.mute(true);           // mutar
handle.mute(false);          // desmutar
handle.setCamera(true);      // ligar camera
handle.hangup();             // desligar

await handle.done;           // aguardar finalizacao completa

Opcoes

ZAPICall.call({
  // --- Obrigatorios ---
  instanceId: string,              // ID da instancia Z-API
  getToken: () => Promise<string>, // Funcao que retorna o token efemero
  number: string,                  // Numero para ligar

  // --- Opcionais ---
  isVideo: false,            // Chamada de video (default: false)
  position: 'bottom-right',  // Posicao inicial do widget
  theme: { ... },            // Personalizacao visual (ver Temas)
});

Chamadas simultaneas

Voce pode fazer multiplas chamadas ao mesmo tempo com call(). Cada chamada cria sua propria instancia independente:

const call1 = ZAPICall.call({
  instanceId: "SUA_INSTANCE_ID",
  getToken: async () => { /* ... */ },
  number: "5511999999999",
});

const call2 = ZAPICall.call({
  instanceId: "SUA_INSTANCE_ID",
  getToken: async () => { /* ... */ },
  number: "5511888888888",
});

// Encerrar uma nao afeta a outra
call1.hangup();

Via CDN (script tag)

<button id="btn-ligar">Ligar para o cliente</button>

<script src="https://unpkg.com/@z-api/call/dist/z-api-call.global.js"></script>
<script>
  document.getElementById("btn-ligar").addEventListener("click", () => {
    const handle = ZAPICall.call({
      instanceId: "SUA_INSTANCE_ID",
      getToken: async () => {
        const res = await fetch("https://seu-backend.com/call-token");
        const data = await res.json();
        return data.token;
      },
      number: "5511999999999",
    });

    handle.done.then(() => {
      console.log("Ligacao finalizada");
    });
  });
</script>

React

import { useCallback } from "react";
import { ZAPICall } from "@z-api/call";
import type { ZAPICallHandle } from "@z-api/call";

function ContactCard({ phone }: { phone: string }) {
  const handleCall = useCallback(() => {
    const handle = ZAPICall.call({
      instanceId: "SUA_INSTANCE_ID",
      getToken: async () => {
        const res = await fetch("/api/call-token");
        const { token } = await res.json();
        return token;
      },
      number: phone,
    });

    handle.done.then(() => {
      console.log("Chamada finalizada");
    });
  }, [phone]);

  return (
    <div>
      <p>Cliente: {phone}</p>
      <button onClick={handleCall}>Ligar</button>
    </div>
  );
}

Opções de Inicialização (ZAPICall.init)

const client = init({
  // --- Obrigatórios ---
  instanceId: string,                    // ID da instância Z-API
  getToken: () => Promise<string>,       // Função que retorna o token efêmero

  // --- Opcionais ---
  autoWidget: true,            // Criar widget automaticamente (default: true)
  showVideoDialButton: false,  // Exibir botão de vídeo chamada no discador (default: false)
  position: 'bottom-right',   // Posição do widget (ver abaixo)
  theme: { ... },             // Personalização visual (ver Temas)

  // --- Callbacks ---
  onIncomingCall: (call) => {},      // Chamada recebida
  onCallTerminated: (callId) => {},  // Chamada encerrada
  onConnectionChange: (connected) => {}, // Conexão mudou
  onMakeCall: (number) => {},        // Chamada de saída iniciada
});

Posições do Widget

| Valor | Posição | | ---------------- | -------------------------------- | | 'bottom-right' | Canto inferior direito (default) | | 'bottom-left' | Canto inferior esquerdo | | 'top-right' | Canto superior direito | | 'top-left' | Canto superior esquerdo |

Temas

Dark / Light Mode

// Tema escuro (default)
const client = init({
  instanceId: "...",
  getToken: async () => {
    /* ... */
  },
  theme: { mode: "dark" },
});

// Tema claro
const client = init({
  instanceId: "...",
  getToken: async () => {
    /* ... */
  },
  theme: { mode: "light" },
});

Personalização Completa

Todas as propriedades são opcionais. Os valores default dependem do mode.

theme: {
  mode: 'dark',              // 'dark' | 'light'
  primaryColor: '#00a884',   // Cor principal (botões, destaques)
  dangerColor: '#f44336',    // Cor de perigo (rejeitar, encerrar)
  backgroundColor: '#202c33',// Fundo dos cards e numpad
  surfaceColor: '#111b21',   // Fundo do painel
  textColor: '#e9edef',      // Cor do texto principal
  textSecondaryColor: '#8696a0', // Cor do texto secundário
  borderRadius: '16px',      // Raio de borda do painel
  fontFamily: 'Inter, sans-serif', // Família de fontes
}

Defaults por modo:

| Propriedade | Dark | Light | | -------------------- | --------- | --------- | | backgroundColor | #202c33 | #f0f2f5 | | surfaceColor | #111b21 | #ffffff | | textColor | #e9edef | #111b21 | | textSecondaryColor | #8696a0 | #667781 |

API do Cliente

Métodos

accept(callId: string): void

Aceita uma chamada recebida.

client.on("call:incoming", (call) => {
  client.accept(call.callId);
});

reject(callId: string): void

Rejeita ou encerra uma chamada.

client.reject(callId);

makeCall(number: string, isVideo?: boolean): void

Inicia uma chamada de saída (áudio ou vídeo). O widget abre automaticamente.

client.makeCall("5511999999999"); // chamada de áudio
client.makeCall("5511999999999", true); // chamada de vídeo

Nota: O widget abre automaticamente ao chamar makeCall. Para o fluxo completo com auto-destroy, use ZAPICall.call().

setCamera(enabled: boolean): Promise<void>

Liga ou desliga a câmera durante uma chamada. Se a chamada for de áudio, automaticamente solicita upgrade para vídeo.

await client.setCamera(true); // ligar câmera
await client.setCamera(false); // desligar câmera

isCameraActive(): boolean

Retorna se a câmera está ativa.

if (client.isCameraActive()) {
  console.log("Câmera ligada");
}

mute(muted: boolean): void

Ativa/desativa o microfone.

client.mute(true); // mutar
client.mute(false); // desmutar

getCalls(): CallInfo[]

Retorna todas as chamadas ativas.

const calls = client.getCalls();
calls.forEach((c) => console.log(c.callId, c.state));

getActiveCall(): CallInfo | undefined

Retorna a chamada ativa (accepted/active) se houver.

const call = client.getActiveCall();
if (call) {
  console.log("Em ligação com", call.from);
}

isConnected(): boolean

Verifica se está conectado ao servidor.

if (client.isConnected()) {
  console.log("Online");
}

openWidget(): void

Abre o painel do widget programaticamente.

client.openWidget();

closeWidget(): void

Fecha o painel do widget programaticamente.

client.closeWidget();

destroy(): void

Destrói o cliente, fecha conexões e remove o widget do DOM.

client.destroy();

Eventos

// Conexão estabelecida
client.on("connected", () => {});

// Conexão perdida
client.on("disconnected", () => {});

// Chamada recebida
client.on("call:incoming", (call: CallInfo) => {
  console.log(call.callId, call.from, call.isVideo);
});

// Estado da chamada mudou
client.on("call:state", (callId: string, state: CallState) => {
  // state: 'ringing' | 'preaccepted' | 'accepted' | 'active' | 'ended'
});

// Chamada encerrada
client.on("call:terminated", (callId: string, reason?: string) => {});

// Vídeo ativado/desativado durante a chamada (upgrade/downgrade)
client.on("call:video-state", (callId: string, isVideo: boolean) => {
  console.log(
    isVideo ? "Chamada mudou para vídeo" : "Chamada voltou para áudio",
  );
});

// Chamada de saída iniciada
client.on("call:outgoing", (call: CallInfo) => {});

// Chamada de saída iniciada (número)
client.on("call:make", (number: string) => {});

// Nível de áudio (0 a 1)
client.on("audio:level", (level: number) => {
  console.log("Volume:", Math.round(level * 100) + "%");
});

// Erro
client.on("error", (error: Error) => {
  console.error(error.message);
});

Para remover um listener:

const handler = (call) => console.log(call);
client.on("call:incoming", handler);
client.off("call:incoming", handler);

Widget

O widget é criado automaticamente quando autoWidget está ativo (default). Ele renderiza dentro de um Shadow DOM isolado, sem conflitos de CSS com a aplicação host.

Estrutura

O widget possui 3 views:

Discador — Teclado numérico para digitar e iniciar chamadas.

Chamadas — Lista de chamadas ativas com botões de aceitar/rejeitar/encerrar.

Ligação Ativa — Tela fullscreen (no painel) com avatar, número, timer, barra de áudio e controles de mute/encerrar.

Comportamento Automático

  • Abre o painel automaticamente ao receber uma chamada
  • Muda para a view "Chamadas" quando tem chamada entrando
  • Muda para a view "Ligação Ativa" quando a chamada é aceita
  • Volta para "Discador" quando todas as chamadas encerram
  • Badge no bubble mostra número de chamadas ativas
  • Dot verde/vermelho indica status da conexão
  • Pulse animation no bubble quando tem chamada tocando

Modo Headless

Para controlar tudo manualmente sem widget:

const client = init({
  instanceId: "...",
  getToken: async () => {
    /* ... */
  },
  autoWidget: false,
});

// Gerencie chamadas via API
client.on("call:incoming", (call) => {
  // Sua UI customizada
});

Tipos

interface CallInfo {
  callId: string; // ID único da chamada
  from: string; // Número de origem
  isVideo: boolean; // Se é vídeo chamada
  isGroup: boolean; // Se é chamada de grupo
  state: CallState; // Estado atual
  timestamp: number; // Timestamp de início
}

type CallState = "ringing" | "preaccepted" | "accepted" | "active" | "ended";

type WidgetPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";

type ThemeMode = "dark" | "light";

interface AudioConfig {
  sampleRate: number; // Taxa de amostragem (ex: 16000)
  channels: number; // Canais de áudio (1 = mono)
}

Protocolo WebSocket

A SDK se conecta automaticamente ao servidor via WebSocket e gerencia reconexão com backoff exponencial. O token efêmero obtido via getToken é utilizado em cada conexão/reconexão.

URL de Conexão

wss://call.z-api.io/ws/call?instance=SUA_INSTANCE_ID&token=TOKEN_EFEMERO

Mensagens Client -> Server

| Tipo | Payload | Descrição | | ------------- | ---------------------- | -------------------------------------------------------- | | call:accept | { callId } | Aceitar chamada | | call:reject | { callId } | Rejeitar/encerrar chamada | | call:mute | { muted } | Mutar/desmutar mic | | call:camera | { enabled } | Ligar/desligar câmera (upgrade para vídeo se necessário) | | call:make | { number, isVideo? } | Iniciar chamada de saída (áudio ou vídeo) | | ping | {} | Keep-alive (automático, 25s) | | binary 0x01 | ArrayBuffer | Áudio PCM Int16LE do microfone | | binary 0x02 | ArrayBuffer | Frame de vídeo NV12 (width u16 + height u16 + data) |

Mensagens Server -> Client

| Tipo | Payload | Descrição | | ------------------ | ------------------------------------------------------- | --------------------------------------------- | | connected | { instanceId, sdkId, audioConfig } | Conexão estabelecida | | call:incoming | { callId, from, isVideo, isGroup, state?, timestamp } | Chamada recebida | | call:outgoing | { callId, to, isVideo } | Chamada de saída iniciada | | call:state | { callId, state } | Mudança de estado | | call:video-state | { callId, isVideo } | Vídeo ativado/desativado durante chamada | | call:terminated | { callId, reason? } | Chamada encerrada | | call:claimed | { callId } | Chamada aceita por outro cliente | | call:dismissed | { callId } | Chamada rejeitada por outro cliente | | pong | { ts } | Resposta ao ping | | error | { command, message } | Erro de processamento | | binary 0x00 | ArrayBuffer | Áudio PCM Int16LE para playback | | binary 0x01 | ArrayBuffer | Frame de vídeo (header 16 bytes + pixel data) |

Áudio

A SDK gerencia captura e reprodução de áudio automaticamente:

  • Formato: PCM Int16LE, mono
  • Sample rate: Configurável pelo servidor (default: 16000 Hz)
  • Captura: Microfone com echo cancellation, noise suppression e auto gain
  • Playback: Web Audio API com scheduling otimizado
  • Permissão: O microfone é solicitado automaticamente ao aceitar uma chamada

Importante: Navegadores exigem interação do usuário antes de reproduzir áudio. A SDK chama AudioContext.resume() automaticamente quando o usuário clica no widget.

Vídeo

A SDK suporta chamadas de vídeo e upgrade de áudio para vídeo durante a chamada:

  • Formato de recepção: NV12 com header (width, height, format, orientation, keyframe, timestamp)
  • Formato de envio: NV12 (RGBA capturado da câmera, convertido para NV12 no browser)
  • Renderização: WebCodecs API (hardware-accelerated) com fallback para software
  • Câmera: Captura a 320x240 @ 30fps (default)
  • Orientação: Suporte a rotação 0/90/180/270 graus

Fluxos suportados

| Cenário | Comportamento | | -------------------------------------------------- | ------------------------------------------------------- | | Chamada inicia como vídeo | Câmera liga automaticamente ao aceitar | | Chamada inicia como áudio, peer muda para vídeo | Vídeo remoto é exibido, câmera liga pelo botão | | Chamada inicia como áudio, usuário muda para vídeo | Clica no botão de câmera, envia upgrade request ao peer | | Discador com botão de vídeo | Habilitado via showVideoDialButton: true |

Exemplos

Integração Básica

<!DOCTYPE html>
<html>
  <head>
    <title>Meu Webphone</title>
  </head>
  <body>
    <h1>Minha Aplicação</h1>
    <script src="https://unpkg.com/@z-api/call/dist/z-api-call.global.js"></script>
    <script>
      const client = ZAPICall.init({
        instanceId: "minha-instancia",
        getToken: async () => {
          const res = await fetch("https://seu-backend.com/call-token");
          const data = await res.json();
          return data.token;
        },
        theme: { mode: "light" },
      });
    </script>
  </body>
</html>

React

import { useEffect, useRef } from "react";
import { init, ZAPICallClient } from "@z-api/call";

function App() {
  const clientRef = useRef<ZAPICallClient | null>(null);

  useEffect(() => {
    const client = init({
      instanceId: "minha-instancia",
      getToken: async () => {
        const res = await fetch("/api/call-token");
        const { token } = await res.json();
        return token;
      },
      position: "bottom-left",
      theme: { mode: "dark", primaryColor: "#7c3aed" },
      onIncomingCall: (call) => {
        console.log("Chamada de", call.from);
      },
    });

    clientRef.current = client;
    return () => client.destroy();
  }, []);

  return <div>Minha App</div>;
}

Headless com UI Customizada

import { init } from "@z-api/call";

const client = init({
  instanceId: "...",
  getToken: async () => {
    const res = await fetch("/api/call-token");
    const { token } = await res.json();
    return token;
  },
  autoWidget: false,
});

client.on("connected", () => {
  document.getElementById("status")!.textContent = "Online";
});

client.on("call:incoming", (call) => {
  const accept = confirm(`Chamada de ${call.from}. Aceitar?`);
  if (accept) {
    client.accept(call.callId);
  } else {
    client.reject(call.callId);
  }
});

client.on("call:state", (callId, state) => {
  document.getElementById("call-state")!.textContent = state;
});

client.on("audio:level", (level) => {
  document.getElementById("meter")!.style.width = `${level * 100}%`;
});

Compatibilidade

  • Chrome 80+
  • Firefox 78+
  • Safari 14+
  • Edge 80+

Requer suporte a:

  • WebSocket
  • Web Audio API
  • getUserMedia (para microfone e câmera)
  • Shadow DOM
  • Canvas 2D (para renderização de vídeo)
  • WebCodecs API (opcional, para renderização acelerada por hardware)

Licença

Proprietary - Four Pixel IT