Example: Placing an outbound call

View as MarkdownOpen in Claude

This example starts a VoxEngine session, places an outbound PSTN call, and bridges audio to Ultravox after the callee answers.

⬇️ Jump to the Full VoxEngine scenario.

Prerequisites

About outbound Caller ID

VoxEngine.callPSTN(...) requires a valid callback-capable caller ID (for example, a rented Voximplant number or a verified caller ID). See https://voximplant.com/docs/getting-started/basic-concepts/phone-numbers.

Launch the routing rule

For quick testing, you can start this outbound scenario from the Voximplant Control Panel:

  1. Open your Voximplant application and go to the Routing tab.
  2. Select the routing rule that has this scenario attached.
  3. Click Run.
  4. Provide Custom data (max 200 bytes) with destination and callerId:
Custom data example
1{"destination":"+15551234567","callerId":"+15557654321"}

For production, start the routing rule via Management API startScenarios (pass rule_id, and pass the same JSON string in script_custom_data): https://voximplant.com/docs/references/httpapi/scenarios#startscenarios

Place the outbound call

Outbound calls are placed with VoxEngine.callPSTN(number, callerid, parameters?).

In the full example, see VoxEngine.customData(), the destination / callerId parse, and the AppEvents.Started handler:

Place the call
1call = VoxEngine.callPSTN(destination, callerId);
2call.addEventListener(CallEvents.Connected, async () => {
3 // ...
4});

Alternate outbound destinations

The example includes commented‑out alternatives to place calls to other endpoints. Use these when you want to avoid PSTN or route internally:

  • Voximplant users (VoxEngine.callUser): calls another app user inside the same Voximplant application. Best for internal testing or app‑to‑app flows.
  • SIP (VoxEngine.callSIP): dial a SIP URI to reach a PBX, carrier, or SIP trunk. Best for enterprise SIP endpoints.
  • WhatsApp (VoxEngine.callWhatsappUser): place a WhatsApp Business‑initiated call (requires a WhatsApp Business account and enabled numbers).

Relevant guides:

Session setup

Ultravox uses a WebSocket API client created via Ultravox.createWebSocketAPIClient(...). The key inputs are:

  • endpoint: use Ultravox.HTTPEndpoint.CREATE_CALL (or CREATE_AGENT_CALL).
  • authorizations: include X-API-Key with your Ultravox API key.
  • body: provide systemPrompt, model, and voice.

Connect call audio

For outbound, create the Ultravox client and bridge audio after the callee answers (so the agent doesn’t speak into ringback):

Create the client and bridge audio
1voiceAIClient = await Ultravox.createWebSocketAPIClient({ ... });
2VoxEngine.sendMediaBetween(call, voiceAIClient);

Barge-in

Clear the output buffer when the caller starts talking to keep the conversation interruption‑friendly:

Barge-in
1voiceAIClient.addEventListener(Ultravox.WebSocketAPIEvents.Transcript, (event) => {
2 const { role } = event?.data?.payload || {};
3 if (role === "user") voiceAIClient.clearMediaBuffer();
4});

Full VoxEngine scenario

voxeengine-ultravox-place-outbound-call.js
1/**
2 * Voximplant + Ultravox WebSocket API connector demo
3 * Scenario: place an outbound call and bridge it to Ultravox after answer.
4 */
5
6require(Modules.Ultravox);
7require(Modules.ApplicationStorage);
8
9const SYSTEM_PROMPT = `You are a helpful phone assistant for Voximplant callers.
10Keep responses short and telephony-friendly (1–2 sentences).`;
11
12// -------------------- Ultravox WebSocket API settings --------------------
13const MODEL = "ultravox-v0.7";
14const VOICE_NAME = "Mark";
15const DEFAULT_CALL_DURATION_MS = 40000;
16
17function parseCustomData() {
18 const raw = VoxEngine.customData();
19 if (!raw) return {};
20 try {
21 return JSON.parse(raw);
22 } catch (error) {
23 Logger.write("===CUSTOM_DATA_PARSE_FAILED===");
24 Logger.write(error);
25 return {};
26 }
27}
28
29VoxEngine.addEventListener(AppEvents.Started, async () => {
30 let voiceAIClient;
31
32 const {
33 destination,
34 callerId,
35 callDurationMs = DEFAULT_CALL_DURATION_MS,
36 } = parseCustomData();
37
38 if (!destination || !callerId) {
39 Logger.write("===MISSING_CUSTOM_DATA===");
40 Logger.write(JSON.stringify({destination, callerId}));
41 VoxEngine.terminate();
42 return;
43 }
44
45 // Place the outbound call (primary path: PSTN)
46 let call = VoxEngine.callPSTN(destination, callerId);
47
48 // Alternative outbound paths (uncomment to use):
49 // call = VoxEngine.callUser({ username: "callee", callerid: callerId });
50 // call = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, callerId);
51 // call = VoxEngine.callWhatsappUser({ number: destination, callerid: callerId });
52
53 let hangupTimer;
54
55 // Termination functions - add cleanup and logging as needed
56 const terminate = () => {
57 if (hangupTimer) clearTimeout(hangupTimer);
58 voiceAIClient?.close();
59 VoxEngine.terminate();
60 };
61
62 call.addEventListener(CallEvents.Disconnected, terminate);
63 call.addEventListener(CallEvents.Failed, terminate);
64
65 call.addEventListener(CallEvents.Connected, async () => {
66 hangupTimer = setTimeout(() => call.hangup(), Number(callDurationMs));
67
68 const apiKey = (await ApplicationStorage.get("ULTRAVOX_API_KEY")).value;
69 const authorizations = {
70 "X-API-Key": apiKey,
71 };
72
73 // Create client and wire media after the callee answers
74 const webSocketAPIClientParameters = {
75 endpoint: Ultravox.HTTPEndpoint.CREATE_CALL,
76 authorizations,
77 body: {
78 systemPrompt: SYSTEM_PROMPT,
79 model: MODEL,
80 voice: VOICE_NAME,
81 },
82 onWebSocketClose: (event) => {
83 Logger.write("===ULTRAVOX_WEBSOCKET_CLOSED===");
84 if (event) Logger.write(JSON.stringify(event));
85 terminate();
86 },
87 };
88
89 try {
90 voiceAIClient = await Ultravox.createWebSocketAPIClient(
91 webSocketAPIClientParameters,
92 );
93
94 VoxEngine.sendMediaBetween(call, voiceAIClient);
95
96 // ---------------------- Event handlers -----------------------
97 // Barge-in: keep conversation responsive and capture transcript
98 voiceAIClient.addEventListener(
99 Ultravox.WebSocketAPIEvents.Transcript,
100 (event) => {
101 const payload = event?.data?.payload || event?.data || {};
102 const role = payload.role;
103 const text = payload.text || payload.delta;
104
105 if (role && text) Logger.write(`===TRANSCRIPT=== ${role}: ${text}`);
106 if (role === "user") voiceAIClient.clearMediaBuffer();
107 },
108 );
109
110 voiceAIClient.addEventListener(
111 Ultravox.WebSocketAPIEvents.PlaybackClearBuffer,
112 () => voiceAIClient.clearMediaBuffer(),
113 );
114
115 // Consolidated "log-only" handlers - key Ultravox/VoxEngine debugging events
116 [
117 Ultravox.WebSocketAPIEvents.ConnectorInformation,
118 Ultravox.WebSocketAPIEvents.HTTPResponse,
119 Ultravox.WebSocketAPIEvents.State,
120 Ultravox.WebSocketAPIEvents.Debug,
121 Ultravox.WebSocketAPIEvents.WebSocketError,
122 Ultravox.WebSocketAPIEvents.Unknown,
123 Ultravox.Events.WebSocketMediaStarted,
124 Ultravox.Events.WebSocketMediaEnded,
125 ].forEach((eventName) => {
126 voiceAIClient.addEventListener(eventName, (event) => {
127 Logger.write(`===${event.name}===`);
128 Logger.write(JSON.stringify(event));
129 });
130 });
131 } catch (error) {
132 Logger.write("===SOMETHING_WENT_WRONG===");
133 Logger.write(error);
134 terminate();
135 }
136 });
137});