Skip to main content
A minimal Bun server that answers a Vobiz call with a <Stream> XML, accepts the inbound WebSocket, and writes each call’s audio to a local WAV file. Use it as a sink-only reference when bringing up a new agent stack - replace the WAV writer with your own STT/LLM/TTS pipeline once frames are flowing.

Run the example

bun install && bun start - server listens on port 3000 by default.

What it does

The server exposes three endpoints: It is sink-only - it does not send playAudio / checkpoint / clearAudio / stop packets back to Vobiz. The full bidirectional protocol is documented in Stream Events, Play Audio, Checkpoint Event, and Clear Audio; extend index.js with those once you wire in an agent.

Run it

To change the port:
Expose the local server with ngrok and update the two URL constants in index.js:

VobizXML answer response

GET / returns:
See Initiate a Stream for the full set of <Stream> attributes.

What the handler does with each frame

  1. GET / returns the <Response><Stream>…</Stream></Response> XML.
  2. On WebSocket /stream open, allocates recordings/call-<ISO timestamp>.wav.
  3. On every media frame, Base64-decodes media.payload, converts it to the WAV representation used by this example, and appends it to the buffer.
  4. On close, flushes the buffered PCM to disk as a mono 16-bit WAV.
  5. On POST /webhook, appends each status event to webhook-events.log as one JSON line.

Sample console output

A live call produces one media frame every 20 ms. A typical mid-stream frame looks like:
Mid-stream media frame

The last media frame

When the caller hangs up, the final media frame arrives, the WebSocket closes, and the buffered PCM is flushed to a WAV. There is no special marker inside the last frame - its shape is identical to every other media frame; stream end is signalled by the stop event (and the subsequent socket close), not by the frame contents. In this real example, sequenceNumber: 274 was the final frame - its payload happens to be all 0xFF bytes (L16 -1, i.e. silence as the carrier wound the call down), but that is a property of the audio at that instant, not an end-of-stream indicator:
Final media frame, immediately followed by socket close + WAV flush
Do not detect end-of-call by inspecting media.payload (e.g. looking for all-/ base64 or all-0xFF bytes). That pattern is just silence at 8 kHz L16 - it can appear mid-call during any pause.End-of-stream signals, in order of arrival:
  1. The WebSocket close event - universal across every termination path. Vobiz does not send an inbound { "event": "stop" }; the socket simply closes after the final media frame. See Detecting end of stream.
  2. Event=StopStream on statusCallbackUrl - fires only for server-initiated stops (when your code sends a stop packet). Does not fire for caller-hangup or mid-stream-kill.
  3. Event=Hangup on hangup_url - fires in every case. This is the authoritative “call is over” signal.
The example server flushes the WAV in its WebSocket close handler, which is why the “WAV recording saved” line is the last log entry of a call.

Audio format

The example’s <Stream> element negotiates:
  • Encoding: L16 (linear 16-bit PCM)
  • Sample rate: 8000 Hz
  • Channels: Mono
  • Chunk size: 20 ms (320 bytes per frame at 8 kHz L16)
For agent use cases, switch to μ-law by adding contentType="audio/x-mulaw;rate=8000" to the <Stream> tag - the JSON envelope is unchanged, only the bytes inside media.payload differ.

Output files

Each WebSocket connection produces a timestamped WAV. Webhook events are appended as one JSON line per event with a receivedAt timestamp.

Extending it

To turn this sink into a full agent, send these frames back over the same socket - see the linked protocol pages for the exact JSON shape:
  • playAudio - queue a 20 ms chunk for playback.
  • checkpoint - mark end of an utterance; Vobiz replies with playedStream once it’s actually played.
  • clearAudio - drop queued audio on barge-in; Vobiz replies with clearedAudio.
  • stop - terminate the call leg from the WebSocket side without a second REST round-trip. Equivalent REST option: POST /audio-streams/.../stop.