WhatsApp audio calls in OpenWA: greetings, streams, recordings and a dashboard
A complete guide to the licensed audio preview: answer and start calls, attach files or streams, record received audio, and control a call across dashboard pages.
WhatsApp audio calling is a major addition to OpenWA's developer surface. An application can answer a call with a greeting, start an outgoing call, receive the other person's audio, and keep controlling the conversation from the SDK or the Calls dashboard.
The public API and dashboard shipped in the 5.5 release. Calling remains a licensed audio preview and needs its corresponding licensed runtime patches. Installing the package alone doesn't activate that private implementation. Camera calls and ready-made telephony or AI-provider adapters are still in development.
Start with a greeting and a recording
Use Node.js 22.21.1 or later and install the current package:
npm install @open-wa/wa-automate@5.5.1
Keep your licence key in your application's environment. The main package includes the browser driver and audio decoder, so the basic example needs no separate media server:
import { create } from '@open-wa/wa-automate';
const client = await create({
licenseKey: process.env.OPENWA_LICENSE_KEY,
});
await client.onIncomingCall(async call => {
await call.accept('greeting.wav', 'caller.wav');
});
The first argument supplies audio to the caller, in place of the microphone. The second receives audio from the caller and writes it to a file. Both paths belong to the machine running this client. Choose a greeting file that exists there and an output directory the process can write to.
OpenWA prepares the source and recording before answering. The greeting starts when the call becomes active, plays once, then leaves the microphone silent while the conversation stays open. Returning from the callback doesn't close the attachment. At call end, OpenWA releases the source and finishes the recording. If an output file already exists, it chooses a new filename rather than replacing the existing recording.
WAV files give the recording a familiar container. Raw .pcm defaults to signed 16-bit little-endian samples at 16 kHz, mono. Encoded sources are detected from their container headers; renaming an incompatible file doesn't change its format.
Use the browser's devices
For a person speaking through the browser that owns the WhatsApp session, omit both arguments:
await client.onIncomingCall(call => call.accept());
That browser needs microphone permissions and working devices. A headless server without devices should attach files or application streams. DEFAULT_MIC and DEFAULT_SPEAKER let you make either default explicit when mixing roles:
import { DEFAULT_SPEAKER } from '@open-wa/wa-automate';
await client.onIncomingCall(call =>
call.accept('greeting.wav', DEFAULT_SPEAKER)
);
This sends a file and plays received audio through the browser's speaker. Pass null to disable a role: call.accept('greeting.wav', null) sends without a receive sink; call.accept(null, 'caller.wav') records without sending microphone audio.
Feed live application audio
A voice application can supply a Web ReadableStream<Uint8Array> as its microphone and consume a Web WritableStream<Uint8Array> as its speaker:
// microphone and speaker are streams owned by your application.
await client.onIncomingCall(call => call.accept(microphone, speaker));
Raw stream input and received output use 16-bit little-endian PCM, 16 kHz, mono. The attachment owns its stream reader and writer and releases them when the call ends. Your application supplies the processing logic; it doesn't need to manage internal Effect scopes or attachment identifiers.
HTTP audio URLs and supported HLS URLs can also supply audio. HLS introduces buffering, so an application stream is generally a better fit for a conversation. A compatible WebSocket source or sink must speak the OpenWA audio protocol and announce its format. An arbitrary provider's wss:// URL isn't a complete integration: its framing, sample format and lifecycle still need an adapter. This preview doesn't include turnkey Twilio, Discord or AI voice-provider adapters.
For looping audio, a different raw sample rate, or an authenticated URL, use a media options object. For example:
await call.accept({
microphone: {
kind: 'file',
path: 'hold-music.pcm',
raw: { encoding: 'pcm16le', sampleRate: 48_000, channels: 2 },
loop: true,
},
speaker: { kind: 'file', path: 'caller.wav' },
});
Start a call and follow its state
const result = await client.startCall(
'447700900123',
'greeting.wav',
'caller.wav',
);
console.log(result);
await client.onCallState(call => {
console.log(call.direction, call.state);
if (call.terminal) console.log(call.endReason);
});
{ ok: true, status: 'requested' } means the native action accepted the request. Follow onCallState for observed progress. Neither a requested state nor an active state proves that the recipient heard audio, and OpenWA doesn't automatically redial an uncertain request.
Incoming calls can be rejected with call.reject(). During an active call, use the current Call object from a callback or fetch it with client.getActiveCall():
const call = await client.getActiveCall();
if (call) {
await call.mute();
await call.unmute();
await call.setMedia({
microphone: 'next-prompt.wav',
speaker: 'next-recording.wav',
});
// When the conversation is complete:
await call.end();
}
setMedia() prepares the replacement before switching, then releases the old attachment. There is one media writer per call, controlled by the client that owns it. clearAudio() clears outgoing audio; for a finite greeting, it stops that greeting.
A separate receive-only observer can record without taking over the microphone:
const observer = await call.observeAudio('audit.wav');
if (observer.ok) await observer.stop();
Observers close at call end. A slow or failed observer closes its own sink instead of accumulating unbounded audio.
Try it from the Calls dashboard
Start Easy API with your licence:
npx @open-wa/wa-automate@5.5.1 --license-key YOUR_LICENSE_KEY --dashboard
Open /dashboard/calls on the printed API address. Search for a contact by name, number or LID and select it, or enter an international number and select that destination. Choose Bundled OpenWA greeting, enable Record received audio, then choose Start audio call. The greeting starts after the other person answers. End the call and use Download received audio for the completed recording.
For an incoming call, the page offers Answer and Reject. Choose your microphone, an audio file, or an HTTP audio URL for the source. The dashboard's browser owns its local files and devices; it asks for microphone permission when needed. HTTP URLs used by this browser need CORS access.
The Chat page also has a Call action in the conversation header. A compact call control at the top of the dashboard shows ringing, connection progress and connected time. Expand it to answer, reject, mute or end a call from another dashboard page. Navigation preserves the call and its audio settings. Reloading the browser closes its media attachment.
Incoming video offers are labelled, and can be answered with audio. The dashboard doesn't yet send camera video or render received video.
Connect an application on another machine
The SocketClient helper resolves local files and streams on the machine running that helper:
import { SocketClient } from '@open-wa/socket-client';
const client = await SocketClient.connect('http://127.0.0.1:8080');
await client.onIncomingCall(call =>
call.accept('greeting.wav', 'caller.wav')
);
A direct HTTP media descriptor instead resolves on the API host. These are different filesystem boundaries. Use the API's configured authentication and HTTPS/WSS when exposing it remotely. Disconnecting or reloading the helper releases its attachment; reconnect and attach fresh sources with a current Call object.
What happens when audio pauses or fails
The microphone sends silence when a live source temporarily has no data, then resumes when that stream supplies more. A finished file leaves the call open and received audio keeps playing or recording. A failed microphone source also leaves the call and receive sink open by default, so your application can replace it with setMedia().
You can explicitly choose onMediaFailure: 'end' when a media failure should hang up. Calling access expiry and browser recovery close devices, sockets and recordings. Old Call objects can't control a new browser generation.
Live input queues hold at most 200 ms of audio. If processing falls behind, OpenWA discards queued samples and logs the discarded duration, preventing an ever-growing conversation delay. You may hear a gap. Received recordings insert silence for missing timestamp intervals so later speech keeps its position; they cannot reconstruct lost speech. Live speaker playback catches up instead of replaying that missing interval.
Expected licence, stale-call and source failures return { ok: false, status, reason } with an actionable log message. Authentication and programming failures remain normal API errors. Terminal snapshots include a readable endReason with its source. An unspecified WhatsApp termination isn't relabelled as a confirmed peer hang-up or network failure.
You don't need a capability-check ritual before each call. Actions handle availability themselves, and an unavailable calling feature doesn't stop the messaging session. The licence menu and client.getLicenseFeatures() expose the feature list if you want to display it in your own UI.
Read the calling guide for the complete workflow and the calls reference for exact wire methods and schemas. Start with one greeting and one recording, then attach the stream processing your application needs.