@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/callVia 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 │ │
└──────────┘ └───────────────┘ └─────────────┘- A SDK chama a função
getTokenfornecida por você - Sua função faz uma requisição ao seu backend
- 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- A Z-API retorna o token efêmero
- Seu backend repassa o token para o frontend
- 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_INSTANCEouclient-tokenno 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 completaOpcoes
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ídeoNota: O widget abre automaticamente ao chamar
makeCall. Para o fluxo completo com auto-destroy, useZAPICall.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âmeraisCameraActive(): 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); // desmutargetCalls(): 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_EFEMEROMensagens 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
