Node.js SDK

Build jambonz voice applications with @jambonz/sdk

The @jambonz/sdk package is the recommended way to build jambonz voice applications in Node.js/TypeScript. It supports both webhook and WebSocket transports, a REST API client for mid-call control, and chainable verb methods for building call flows.

Source code: github.com/jambonz/node-sdk API reference: jambonz.github.io/node-sdk

This SDK replaces the older @jambonz/node-client (webhook) and @jambonz/node-client-ws (WebSocket) packages, which are now deprecated. The new SDK provides a unified package with a consistent API across both transports.

Which transport should I use? The WebSocket transport is recommended for most applications. It provides a persistent bidirectional connection that enables TTS streaming, mid-call updates, inject commands, real-time event handling, and voice AI features like the agent and llm verbs. The webhook transport is simpler but limited — use it for straightforward call routing scenarios (e.g., dial, basic IVR menus) where you don’t need real-time interaction.

Installation

npm install @jambonz/sdk

Imports

The SDK provides three subpath exports:

// Webhook apps (Express/HTTP)
import { WebhookResponse } from '@jambonz/sdk/webhook';
// WebSocket apps
import { createEndpoint } from '@jambonz/sdk/websocket';
// REST API client (mid-call control, outbound calls)
import { JambonzClient } from '@jambonz/sdk/client';

Webhook Transport

Use WebhookResponse to build verb arrays in response to HTTP webhooks. Methods are chainable and the response is serialized to JSON.

import express from 'express';
import { WebhookResponse } from '@jambonz/sdk/webhook';
const app = express();
app.use(express.json());
app.post('/incoming', (req, res) => {
const jambonz = new WebhookResponse();
jambonz
.say({ text: 'Hello from jambonz!' })
.gather({
input: ['speech', 'digits'],
actionHook: '/handle-input',
say: { text: 'Press 1 for sales or 2 for support.' },
})
.hangup();
res.json(jambonz);
});
app.post('/handle-input', (req, res) => {
const jambonz = new WebhookResponse();
const speech = req.body.speech?.alternatives?.[0]?.transcript;
jambonz.say({ text: `You said: ${speech}` }).hangup();
res.json(jambonz);
});
app.listen(3000);

WebSocket Transport

Use createEndpoint to build real-time WebSocket applications. This is the recommended transport for voice AI agents, as it enables bidirectional communication, event streaming, and mid-call updates.

import http from 'http';
import { createEndpoint } from '@jambonz/sdk/websocket';
const server = http.createServer();
const makeService = createEndpoint({ server, port: 3000 });
const svc = makeService({ path: '/' });
svc.on('session:new', (session) => {
// Bind actionHook handlers first
session.on('/gather-result', (evt) => {
const transcript = evt.speech?.alternatives?.[0]?.transcript || '';
session.say({ text: `You said: ${transcript}` }).hangup().reply();
});
// Send initial verbs
session
.say({ text: 'Hello! Say something.' })
.gather({ input: ['speech'], actionHook: '/gather-result', timeout: 10 })
.hangup()
.send();
});

.send() vs .reply()

  • .send() — Use once for the initial verb array in response to session:new.
  • .reply() — Use for all subsequent responses to actionHook events.

This distinction is important: .send() starts the call flow, while .reply() continues it in response to events.

Application Environment Variables

You can declare environment variables that are configurable in the jambonz portal UI:

const makeService = createEndpoint({
server,
port: 3000,
envVars: {
OPENAI_MODEL: {
type: 'string',
description: 'LLM model to use',
default: 'gpt-4.1-mini',
},
SYSTEM_PROMPT: {
type: 'string',
description: 'System prompt',
uiHint: 'textarea',
default: 'You are a helpful assistant.',
},
},
});
// Read values in session handler
svc.on('session:new', (session) => {
const model = session.data.env_vars?.OPENAI_MODEL || 'gpt-4.1-mini';
// ...
});

Audio Streams

When using the listen verb, makeService.audio() lets you handle both call control and audio on the same server:

const svc = makeService({ path: '/' });
const audioSvc = makeService.audio({ path: '/audio-stream' });
svc.on('session:new', (session) => {
session
.say({ text: 'Listening...' })
.listen({
url: '/audio-stream',
sampleRate: 8000,
bidirectionalAudio: { enabled: true, streaming: true, sampleRate: 8000 },
})
.send();
});
audioSvc.on('connection', (stream) => {
stream.on('audio', (pcm) => {
// Process audio — feed to STT, record, etc.
});
// Send audio back
stream.sendAudio(pcmBuffer);
stream.on('close', () => console.log('Audio stream closed'));
});

The AudioStream object provides sendAudio(), playAudio(), killAudio(), disconnect(), sendMark(), and clearMarks() methods.

REST API Client

Use JambonzClient for outbound calls and mid-call control:

import { JambonzClient } from '@jambonz/sdk/client';
const client = new JambonzClient({
baseUrl: 'https://api.jambonz.us',
accountSid: 'your-account-sid',
apiKey: 'your-api-key',
});
// Create an outbound call
await client.calls.create({
from: '+15085551212',
to: { type: 'phone', number: '+15085551213' },
call_hook: '/incoming',
});
// Mid-call control
await client.calls.mute(callSid, 'mute');
await client.calls.redirect(callSid, 'https://example.com/new-flow');

Verb Methods

Both WebhookResponse and WebSocket Session support the same chainable verb methods:

.say() .play() .gather() .dial() .llm() .agent() .conference() .enqueue() .dequeue() .hangup() .pause() .redirect() .config() .tag() .dtmf() .listen() .transcribe() .message() .dub() .alert() .answer() .leave() .sipDecline() .sipRefer() .sipRequest()

All methods accept the same options as the corresponding verb JSON schemas and are chainable.

TTS Token Streaming

The WebSocket Session provides methods for incremental TTS token streaming, enabling the lowest-latency voice AI experiences. This is used when you’re streaming tokens from an LLM and want them spoken as they arrive.

session.on('/llm-tokens', async (evt) => {
const { tokens, done } = evt;
if (tokens) {
// Send tokens as they arrive from the LLM — backpressure is handled automatically
await session.sendTtsTokens(tokens);
}
if (done) {
// Signal end of token stream
session.flushTtsTokens();
}
});
MethodReturnsDescription
sendTtsTokens(text)Promise<void>Send a chunk of text for TTS. Resolves when jambonz acknowledges receipt. Automatically applies backpressure if the buffer is full.
flushTtsTokens()voidSignal the end of a TTS token stream. Triggers final audio generation.
clearTtsTokens()voidCancel all pending TTS tokens, clear the queue, and reset backpressure state.

The isTtsPaused property indicates whether TTS streaming is paused due to backpressure.

TTS Streaming Events

EventDescription
tts:stream_openTTS vendor connection established
tts:stream_pausedBackpressure — buffer full, tokens will queue
tts:stream_resumedBackpressure released, streaming resumes
tts:stream_closedTTS stream ended
tts:user_interruptionUser barged in during TTS playback

LLM and Agent Updates

The Session provides methods for interacting with active LLM and agent conversations.

Tool Output

When the LLM requests a tool/function call (via the toolHook), respond with the result:

session.on('/tool-call', async (evt) => {
const { tool_call_id, name, arguments: args } = evt;
const result = await handleTool(name, args);
session.sendToolOutput(tool_call_id, result);
});

Agent Updates

Send mid-conversation updates to an active agent:

// Change the system prompt
session.updateAgent({
type: 'update_instructions',
instructions: 'You are now a billing agent.',
});
// Inject context
session.updateAgent({
type: 'inject_context',
messages: [{ role: 'user', content: 'Customer: Sarah, Gold tier.' }],
});
// Replace tools
session.updateAgent({ type: 'update_tools', tools: [...] });
// Trigger a new response (with optional interrupt)
session.updateAgent({
type: 'generate_reply',
interrupt: true,
user_input: 'Tell the customer about the flash sale.',
});

LLM Updates

Send updates to an active llm verb:

session.updateLlm({ instructions: 'Switch to Spanish.' });
MethodDescription
sendToolOutput(toolCallId, data)Send tool/function result back to the LLM or agent verb
updateAgent(data)Send an agent:update command (update_instructions, inject_context, update_tools, generate_reply)
updateLlm(data)Send an llm:update command

Inject Commands

Inject commands execute immediately on an active call without affecting the verb stack. They are useful for mid-call control actions like muting, recording, or whispering to one party on a bridged call.

// Mute/unmute
session.injectMute('mute');
session.injectMute('unmute');
// Whisper to one party (e.g., coaching a call center agent)
session.injectWhisper({ verb: 'say', text: 'The customer is a VIP.' }, agentCallSid);
// Control noise isolation mid-call
session.injectNoiseIsolation('enable', { vendor: 'krisp', level: 80 });
session.injectNoiseIsolation('disable');
// Control recording
session.injectRecord('startCallRecording', { siprecServerURL: 'sip:siprec@recorder.example.com' });
session.injectRecord('pauseCallRecording');
// Pause/resume audio streaming
session.injectListenStatus('pause');
session.injectListenStatus('resume');
// Send DTMF
session.injectDtmf('1234');
// Redirect call flow
session.injectRedirect('/new-webhook');
// Tag the call with metadata
session.injectTag({ priority: 'high', department: 'billing' });
MethodDescription
injectMute(status)Mute or unmute the call ('mute' or 'unmute')
injectWhisper(verb, callSid?)Play a whisper verb (say/play) to one party on a bridged call
injectNoiseIsolation(status, opts?, callSid?)Enable or disable noise isolation. Options: vendor, level, model
injectRecord(action, opts?, callSid?)Control call recording: startCallRecording, stopCallRecording, pauseCallRecording, resumeCallRecording
injectListenStatus(status, callSid?)Pause or resume audio streaming ('pause' or 'resume')
injectDtmf(digit, duration?, callSid?)Send DTMF digits into the call
injectRedirect(hook, callSid?)Redirect call execution to a new webhook
injectTag(data, callSid?)Attach metadata to the call
injectCommand(command, data?, callSid?)Send a generic inject command

The optional callSid parameter on inject methods targets a specific call leg on a bridged call. Omit it to target the current call.

Session Properties

PropertyTypeDescription
callSidstringUnique call identifier
fromstringCaller phone number or SIP URI
tostringCalled phone number or SIP URI
direction'inbound' | 'outbound'Call direction
accountSidstringAccount identifier
applicationSidstringApplication identifier
callIdstringSIP Call-ID
dataCallSessionFull call session data (includes env_vars, SIP headers, etc.)
localsRecord<string, unknown>Application-specific storage that persists for the session
isTtsPausedbooleanWhether TTS streaming is paused due to backpressure

Session Events

EventDescription
'/hookName'ActionHook callback — requires .reply()
verb:statusVerb status change (when notifyEvents is enabled)
call:statusCall state change
jambonz:errorError from jambonz
closeWebSocket connection closed
errorWebSocket connection error

AI-Assisted Development

The @jambonz/mcp-schema-server package is an MCP server that gives AI coding assistants deep knowledge of jambonz APIs, verb schemas, and SDK patterns. Set it up so your AI can generate correct jambonz code automatically.

Remote server (simplest):

claude mcp add jambonz -t http https://mcp-server.jambonz.app/mcp

Local via npx:

claude mcp add jambonz -- npx -y @jambonz/mcp-schema-server

For Cursor, VS Code, and other editors, see the setup instructions in the repository.

A complementary Agent Skill provides procedural knowledge about jambonz patterns and best practices:

npx skills add jambonz/skills

Examples

See the examples directory for runnable demos:

ExampleTransportDescription
hello-worldWebhook + WSMinimal greeting
echoWebhook + WSSpeech echo using gather
ivr-menuWebhookInteractive menu with speech and DTMF
voice-agentWebhook + WSLLM-powered conversational AI with tools
openai-realtimeWebSocketOpenAI Realtime API voice agent
llm-streamingWebSocketLLM with TTS streaming and barge-in

For agent verb examples, see the agent examples.