Example: Placing an outbound call

View as MarkdownOpen in Claude

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

  • A Grok API key stored in ApplicationStorage as XAI_API_KEY.
  • 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":"+16174018889","callerId":"+13322776633"}

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);
2require(Modules.ApplicationStorage);
3
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\":\"+16174018889\",\"callerId\":\"+13322776633\"}",
76 );
77 VoxEngine.terminate();
78 return;
79 }
80
81 call = VoxEngine.callPSTN(destination, callerId);
82
83 // Termination functions - add cleanup and logging as needed
84 call.addEventListener(CallEvents.Disconnected, ()=>VoxEngine.terminate());
85 call.addEventListener(CallEvents.Failed, ()=>VoxEngine.terminate());
86
87 try {
88 voiceAIClient = await Grok.createVoiceAgentAPIClient({
89 xAIApiKey: (await ApplicationStorage.get("XAI_API_KEY")).value,
90 onWebSocketClose: (event) => {
91 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
92 VoxEngine.terminate();
93 },
94 });
95
96 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.ConversationCreated, (event) => {
97 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
98 voiceAIClient.sessionUpdate(SESSION_PARAMETERS);
99 });
100
101 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.SessionUpdated, (event) => {
102 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
103 voiceAIClient.responseCreate({instructions: "Hello."});
104 });
105
106 // Keep it interruption-friendly (barge-in).
107 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.InputAudioBufferSpeechStarted, (event) => {
108 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
109 voiceAIClient.clearMediaBuffer();
110 });
111
112 // Function calling.
113 voiceAIClient.addEventListener(Grok.VoiceAgentAPIEvents.ResponseFunctionCallArgumentsDone, (event) => {
114 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
115
116 const {name, call_id} = event?.data?.payload || {};
117 let output;
118
119 if (name !== "forward_to_agent" && name !== "hangup_call") {
120 Logger.write(`===Ignoring unhandled function call: ${name}===`);
121 return;
122 }
123
124 if (name === "forward_to_agent") {
125 forwardToLiveAgent = true;
126 output = {result: "Forwarding your call to a live agent. Please hold on."};
127 } else if (name === "hangup_call") {
128 hangupCall = true;
129 output = {result: "Have a great day, goodbye!"};
130 }
131
132 voiceAIClient.conversationItemCreate({
133 item: {
134 type: "function_call_output",
135 call_id,
136 output: JSON.stringify(output),
137 },
138 });
139 voiceAIClient.responseCreate({});
140 });
141
142 // -------------------- Log Other Events --------------------
143 [
144 CallEvents.FirstAudioPacketReceived,
145 Grok.Events.WebSocketMediaStarted,
146 Grok.VoiceAgentAPIEvents.InputAudioBufferSpeechStopped,
147 Grok.VoiceAgentAPIEvents.ConversationItemInputAudioTranscriptionCompleted,
148 Grok.VoiceAgentAPIEvents.ConversationItemAdded,
149 Grok.VoiceAgentAPIEvents.ResponseCreated,
150 Grok.VoiceAgentAPIEvents.ResponseOutputItemAdded,
151 Grok.VoiceAgentAPIEvents.ResponseDone,
152 Grok.VoiceAgentAPIEvents.ResponseOutputAudioTranscriptDelta,
153 Grok.VoiceAgentAPIEvents.ResponseOutputAudioTranscriptDone,
154 Grok.VoiceAgentAPIEvents.ResponseOutputAudioDelta,
155 Grok.VoiceAgentAPIEvents.ResponseOutputAudioDone,
156 Grok.VoiceAgentAPIEvents.ResponseOutputItemDone,
157 Grok.VoiceAgentAPIEvents.ConnectorInformation,
158 Grok.VoiceAgentAPIEvents.InputAudioBufferCommitted,
159 Grok.VoiceAgentAPIEvents.WebSocketError,
160 Grok.VoiceAgentAPIEvents.Unknown,
161 ].forEach((evtName) => {
162 voiceAIClient.addEventListener(evtName, (e) => {
163 Logger.write(`===${e.name}===>${JSON.stringify(e)}`);
164 });
165 });
166
167 voiceAIClient.addEventListener(Grok.Events.WebSocketMediaStarted, () => {
168 VoxEngine.sendMediaBetween(call, voiceAIClient);
169 });
170
171 voiceAIClient.addEventListener(Grok.Events.WebSocketMediaEnded, (event) => {
172 Logger.write(`===${event.name}===>${JSON.stringify(event.data)}`);
173 if (hangupCall) callCloseHandler();
174 else if (forwardToLiveAgent) {
175 call.say("Here is where I would forward the call via the phone network or SIP.");
176 call.addEventListener(CallEvents.PlaybackFinished, callCloseHandler);
177 } else return;
178 });
179 } catch (error) {
180 Logger.write("===SOMETHING_WENT_WRONG===");
181 Logger.write(error);
182 VoxEngine.terminate();
183 }
184});