Example: Placing an outbound call

View as Markdown

For the complete documentation index, see llms.txt.

Overview

This example starts a VoxEngine session, places an outbound PSTN call, and connects the callee to a Grok voice agent when the media WebSocket is ready.

⬇️ Jump to the Full VoxEngine scenario.

Prerequisites

  • An xAI API key stored in Voximplant Secrets under XAI_API_KEY.
  • A Grok voice model selected in the example code. The default sample explicitly uses grok-voice-think-fast-1.0 until xAI changes the Voice Agent API default on May 31, 2026.
  • A verified or rented caller ID that can place outbound PSTN calls.
  • Start this scenario via a routing rule (for production use the Management API to start the rule).

Custom data

Pass the destination and caller ID when starting the scenario:

1{"destination":"+15551234567","callerId":"+13322776633"}

Alternate outbound destinations

This example uses VoxEngine.callPSTN(...) for PSTN dialing. You can also route outbound calls to other destination types in VoxEngine:

  • SIP (VoxEngine.callSIP): dial a SIP URI to reach a PBX, carrier, SIP trunk, or other SIP endpoint.
  • WhatsApp (VoxEngine.callWhatsappUser): place a WhatsApp Business-initiated call (requires a WhatsApp Business account and enabled numbers).
  • Voximplant users (VoxEngine.callUser): calls another app user inside the same Voximplant application such as web SDK, mobile SDK, or SIP user.

Relevant guides:

Connect call audio

Bridge the PSTN call to Grok once the WebSocket media session is live:

1VoxEngine.sendMediaBetween(call, voiceAgentAPIClient);

Notes

See the VoxEngine API Reference for more details.

Full VoxEngine scenario

voxeengine-grok-place-outbound-call.js
1require(Modules.Grok);
2// Temporary explicit model until xAI changes the Voice Agent API default on May 31, 2026.
3const GROK_MODEL = "grok-voice-think-fast-1.0";
4const SYSTEM_PROMPT = `
5 Your name is Voxi. You are a helpful voice assistant for phone callers representing the company Voximplant (pronounced VOX-im-plant).
6 Keep responses short and telephony-friendly (usually 1-2 sentences).
7 If the user asks for a live agent or an operator, call the "forward_to_agent" function.
8 If the user says goodbye, call the "hangup_call" function.
9`;
10
11// -------------------- Grok Voice Agent settings --------------------
12const SESSION_PARAMETERS = {
13 session: {
14 voice: "Ara",
15 turn_detection: {type: "server_vad"},
16 instructions: SYSTEM_PROMPT,
17 tools: [
18 {type: "web_search"},
19 {
20 type: "file_search",
21 vector_store_ids: ["collection_4c5a63ab-f739-4c13-93d2-05b74095c34a"], // optional RAG
22 max_num_results: 5,
23 },
24 {
25 type: "x_search",
26 allowed_x_handles: ["voximplant", "aylarov"],
27 },
28 {
29 type: "function",
30 name: "forward_to_agent",
31 description: "Forward the user to a live agent",
32 parameters: {
33 type: "object",
34 properties: {},
35 required: [],
36 },
37 },
38 {
39 type: "function",
40 name: "hangup_call",
41 description: "Hangup the call",
42 parameters: {
43 type: "object",
44 properties: {},
45 required: [],
46 },
47 },
48 ],
49 },
50};
51
52// Helper to parse custom data JSON from call setup
53function parseCustomData() {
54 const raw = VoxEngine.customData();
55 if (!raw) return {};
56 try {
57 return JSON.parse(raw);
58 } catch (error) {
59 Logger.write("===CUSTOM_DATA_PARSE_FAILED===");
60 Logger.write(error);
61 return {};
62 }
63}
64
65VoxEngine.addEventListener(AppEvents.Started, async () => {
66 let voiceAIClient;
67 let call;
68 let hangupCall = false;
69 let forwardToLiveAgent = false;
70
71 const {destination, callerId} = parseCustomData();
72
73 if (!destination || !callerId) {
74 Logger.write(
75 "Missing destination or callerId. Provide custom data: {\"destination\":\"+15551234567\",\"callerId\":\"+13322776633\"}",
76 );
77 VoxEngine.terminate();
78 return;
79 }
80
81 call = VoxEngine.callPSTN(destination, callerId);
82 // Alternative outbound paths (uncomment to use):
83 // call = VoxEngine.callUser({username: destination, callerid: callerId});
84 // call = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, callerId);
85 // call = VoxEngine.callWhatsappUser({number: destination, callerid: callerId});
86
87 // Termination functions - add cleanup and logging as needed
88 call.addEventListener(CallEvents.Disconnected, ()=>VoxEngine.terminate());
89 call.addEventListener(CallEvents.Failed, ()=>VoxEngine.terminate());
90
91 try {
92 voiceAIClient = await Grok.createVoiceAgentAPIClient({
93 xAIApiKey: VoxEngine.getSecretValue("XAI_API_KEY"),
94 model: GROK_MODEL,
95 onWebSocketClose: (event) => {
96 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
97 VoxEngine.terminate();
98 },
99 });
100
101 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.ConversationCreated, (event) => {
102 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
103 voiceAIClient.sessionUpdate(SESSION_PARAMETERS);
104 });
105
106 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.SessionUpdated, (event) => {
107 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
108 voiceAIClient.responseCreate({instructions: "Hello."});
109 });
110
111 // Keep it interruption-friendly (barge-in).
112 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.InputAudioBufferSpeechStarted, (event) => {
113 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
114 voiceAIClient.clearMediaBuffer();
115 });
116
117 // Function calling.
118 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.ResponseFunctionCallArgumentsDone, (event) => {
119 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
120
121 const {name, call_id} = event?.data?.payload || {};
122 let output;
123
124 if (name !== "forward_to_agent" && name !== "hangup_call") {
125 Logger.write(`===Ignoring unhandled function call: ${name}===`);
126 return;
127 }
128
129 if (name === "forward_to_agent") {
130 forwardToLiveAgent = true;
131 output = {result: "Forwarding your call to a live agent. Please hold on."};
132 } else if (name === "hangup_call") {
133 hangupCall = true;
134 output = {result: "Have a great day, goodbye!"};
135 }
136
137 voiceAIClient.conversationItemCreate({
138 item: {
139 type: "function_call_output",
140 call_id,
141 output: JSON.stringify(output),
142 },
143 });
144 voiceAIClient.responseCreate({});
145 });
146
147 // -------------------- Log Other Events --------------------
148 [
149 CallEvents.FirstAudioPacketReceived,
150 Grok.Events.WebSocketMediaStarted,
151 Grok.VoiceAgentAPIEvents.InputAudioBufferSpeechStopped,
152 Grok.VoiceAgentAPIEvents.ConversationItemInputAudioTranscriptionCompleted,
153 Grok.VoiceAgentAPIEvents.ConversationItemAdded,
154 Grok.VoiceAgentAPIEvents.ResponseCreated,
155 Grok.VoiceAgentAPIEvents.ResponseOutputItemAdded,
156 Grok.VoiceAgentAPIEvents.ResponseDone,
157 Grok.VoiceAgentAPIEvents.ResponseOutputAudioTranscriptDelta,
158 Grok.VoiceAgentAPIEvents.ResponseOutputAudioTranscriptDone,
159 Grok.VoiceAgentAPIEvents.ResponseOutputAudioDelta,
160 Grok.VoiceAgentAPIEvents.ResponseOutputAudioDone,
161 Grok.VoiceAgentAPIEvents.ResponseOutputItemDone,
162 Grok.VoiceAgentAPIEvents.ConnectorInformation,
163 Grok.VoiceAgentAPIEvents.InputAudioBufferCommitted,
164 Grok.VoiceAgentAPIEvents.WebSocketError,
165 Grok.VoiceAgentAPIEvents.Unknown,
166 ].forEach((evtName) => {
167 voiceAIClient.addEventListener(evtName, (e) => {
168 Logger.write(`===${e.name}===>${JSON.stringify(e)}`);
169 });
170 });
171
172 voiceAIClient.addEventListener(Grok.Events.WebSocketMediaStarted, () => {
173 VoxEngine.sendMediaBetween(call, voiceAIClient);
174 });
175
176 voiceAIClient.addEventListener(Grok.Events.WebSocketMediaEnded, (event) => {
177 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
178 if (hangupCall) callCloseHandler();
179 else if (forwardToLiveAgent) {
180 call.say("Here is where I would forward the call via the phone network or SIP.");
181 call.addEventListener(CallEvents.PlaybackFinished, callCloseHandler);
182 } else return;
183 });
184 } catch (error) {
185 Logger.write("===SOMETHING_WENT_WRONG===");
186 Logger.write(error);
187 VoxEngine.terminate();
188 }
189});