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

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