Skip to main content
@zapo-js/voip is the official voice-calling plugin for zapo-js. It rides on the plugin system, exposes a WaVoipCoordinator at client.voip, and emits voip_* events on the host client.
Only audio media is implemented. You may flag a call as video in the signaling (see CallOfferOptions.isVideo), but no video media is encoded or transported.

Install

The package has three peer dependencies, all required at runtime:
  • @roamhq/wrtc (>= 0.10.0) — provides SCTP for the relay transport.
  • libmlow-wasm (^0.1.1) — WhatsApp’s Opus profile, compiled to WebAssembly. No native build step.
  • zapo-js (^1.0.0) — the host client.
Some methods also need an ffmpeg binary on PATH:
  • loadAudio decodes the audio file via ffmpeg.
  • Streaming audio with feedLiveAudio from a file likewise needs ffmpeg to produce 16 kHz mono Float32Array chunks.

Wire the plugin

Add voipPlugin() to WaClientOptions.plugins. The coordinator appears at client.voip and the voip_* events become available on client.on.

Plugin options

maxConcurrentCalls
number
default:"1"
Maximum simultaneous non-ended calls (ringing, connecting, or active). Increase to enable parallel multi-call. Outgoing startCall rejects, and incoming calls arrive with acceptBlocked: true, when the limit is hit.
logLevel
LogLevel
Minimum log level for the (chatty) VoIP plugin. Defaults to the host client’s level; set it to cap diagnostics independently — for example 'warn' to keep VoIP noise out of a trace host logger.

The coordinator surface

Placing a call

peerJid
string
required
Bare or device JID to call.
isVideo
boolean
default:"false"
Flag the call as video in the signaling stanzas. Video media encode/transport is not implemented; only audio media flows.
audioFile
string
Audio file to preload and play once the call connects (needs ffmpeg). Equivalent to calling loadAudio after the call goes active.
peerDevices
string[]
Explicit peer device JIDs to ring; omit to resolve them automatically.
startCall resolves with the new call id once the offer is sent. Progress then arrives via voip_call_state. It rejects when you’re at the maxConcurrentCalls limit or if the offer fails to send.

Answering and ending

  • acceptCall(callId) — accept a ringing incoming call. Throws if callId is unknown or not in an acceptable state.
  • rejectCall(callId, reason?) — reject a ringing incoming call (default reason Declined). Sends the reject stanza, then tears the call down.
  • endCall(callId, reason?) — end an active or connecting call (default reason UserEnded). No-op if the call is unknown or already ended.

Preloaded outbound audio

Decodes audioPath via ffmpeg and queues it as the outbound audio, played once the call is active. Throws if the file is missing or ffmpeg is unavailable. For an unbounded or live source (TTS stream, ongoing capture) use external audio mode instead — see below.

Mute

Live outbound audio (external mode)

For sources you can’t preload — streaming TTS, real-time capture — switch the call into external audio mode and pump samples in as they arrive. The plugin buffers them through a bounded jitter buffer and watermarks the buffer so a respectful producer never loses audio.
The watermark contract: pause feeding once getLiveBufferMs(callId) >= pauseMs, resume once it drains back to resumeMs. pauseMs stays below the engine’s internal drop threshold, so a producer that respects it never loses audio. If you ignore it, oldest samples are dropped on overflow.

Snapshots

CallInfo captures everything you need to render UI: callId, peerJid, direction, mediaType, createdAt, callerPn, plus a stateData object (state, audioMuted, videoOff, connectedAt, endReason, durationSecs, acceptBlocked, …) and derived getters isInitiator, isActive, isRinging, isEnded, canAccept, canReject, isAcceptBlocked.

Events

Add voipPlugin() to plugins and these events become available on client.on. Without the plugin they don’t exist on the type — see Plugins.

Enums

All enum values are lowercase strings — use the enum constants to avoid typos.

CallState

CallDirection

CallMediaType

EndCallReason

Audio format

  • Sample rate: 16 kHz.
  • Channels: mono.
  • Sample format: Float32Array in [-1.0, 1.0].
  • Codec on the wire: WhatsApp’s Opus profile, encoded via libmlow-wasm. The RTP payload type is 120 (PayloadType.WhatsAppOpus).
Both inbound (voip_call_inbound_audio) and outbound (feedLiveAudio) are this same Float32Array shape — keep your buffers in this format and you can route them straight through.

Worked example

A minimal incoming auto-accept that records inbound PCM into an in-memory buffer:
For a fuller example — placing outgoing calls, preloaded vs streamed playback, hangup-after-audio, WAV recording — see examples/voip-example.ts in the zapo source tree.

See also

Plugin system

How voipPlugin() plugs into WaClient, and how to author your own.

Events

The host client’s event surface; voip_* events sit alongside the built-ins.

Credits

The VoIP plugin was built by:
Last modified on June 30, 2026