Example: Placing an outbound call

View as Markdown

For the complete documentation index, see llms.txt.

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

Jump to the Full VoxEngine scenario.

Prerequisites

  • Create a routing rule with this scenario attached so you can run it from the Control Panel or trigger it with the Management API.
  • Have a PSTN destination number you can dial.
  • Configure a valid outbound callerId, such as a rented Voximplant number or a verified caller ID. See Phone Numbers & PSTN.
  • Store your Inworld API key in Voximplant Secrets under INWORLD_API_KEY.

Outbound call parameters

The example expects destination and callerId in customData passed to the routing rule:

Custom data example
1{"destination":"+15551234567","callerId":"+15557654321"}

Launch the routing rule

For quick testing 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 JSON with destination and callerId.

For production, start the routing rule via the Management API startScenarios method and pass the same JSON in script_custom_data.

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 from an enabled WhatsApp Business number.
  • Voximplant users (VoxEngine.callUser): call another app user inside the same Voximplant application, such as a Web SDK, mobile SDK, or SIP user.

Relevant guides:

Session setup

Outbound setup follows this order:

  1. Parse destination and callerId from VoxEngine.customData().
  2. Place the PSTN call with VoxEngine.callPSTN(...).
  3. Wait for CallEvents.Connected.
  4. Create Inworld.RealtimeAPIClient.
  5. Send the same style of sessionUpdate(...) payload shown in the inbound example after Inworld emits SessionCreated.
  6. Bridge call media to Inworld after SessionUpdated.
  7. Seed the first assistant turn with conversationItemCreate(...) and responseCreate(...).

The scenario deliberately creates and configures the Inworld session only after the callee answers, so the agent does not speak into ringback:

Create Inworld client after callee answers
1call.addEventListener(CallEvents.Connected, async () => {
2 voiceAIClient = await Inworld.createRealtimeAPIClient({
3 apiKey: VoxEngine.getSecretValue("INWORLD_API_KEY"),
4 sessionKey: `inworld-outbound-demo-${Date.now()}`,
5 onWebSocketClose: terminate,
6 });
7});

The full outbound scenario keeps the session config intentionally smaller than the inbound example: it sets the model, prompt, audio input/output models, and the TTS delivery options used by this call flow. For the detailed session config walkthrough, including naturalness and responsiveness tuning, see Answering an incoming call.

After SessionUpdated, connect the PSTN leg to Inworld and trigger the opening turn:

Bridge media and trigger opening
1VoxEngine.sendMediaBetween(call, voiceAIClient);
2voiceAIClient.conversationItemCreate({
3 item: {
4 type: "message",
5 role: "user",
6 content: [{
7 type: "input_text",
8 text: "The outbound call just connected. Say only: Hi, this is Voxi from Voximplant. How can I help?",
9 }],
10 },
11});
12voiceAIClient.responseCreate({ response: { output_modalities: ["audio", "text"] } });

For provider-specific tuning details, see Inworld’s Realtime API Extensions.

The example also adds:

  • CallEvents.Failed and CallEvents.Disconnected handlers for cleanup.
  • A demo hangup timer to avoid indefinitely running sessions.
  • Inworld event logs for session updates, response lifecycle, transcripts, speech starts, and errors.

Barge-in

Callee speech clears queued Inworld output audio so the callee can interrupt the agent naturally:

Barge-in
1voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => {
2 voiceAIClient.outputAudioBufferClear({});
3});

Notes

  • INWORLD_API_KEY is read from Voximplant Secrets.
  • sessionKey can be any unique string to maintain context for the Inworld session.
  • The example includes a demo hangup timer so unattended outbound tests do not run indefinitely.

Full VoxEngine scenario

voxeengine-inworld-outbound-call.js
1/**
2 * Voximplant + Inworld Realtime API demo
3 * Scenario: place an outbound call and bridge it to Inworld Realtime.
4 *
5 * Configure this in the Voximplant application:
6 * - Secret `INWORLD_API_KEY` (Voximplant Secrets)
7 * - Routing rule custom data with `destination` and `callerId`
8 */
9
10require(Modules.Inworld);
11
12const DEFAULT_CALL_DURATION_MS = 2 * 60 * 1000;
13
14const SYSTEM_PROMPT = `
15You are Voxi, a Voximplant developer advocate on a live outbound phone call.
16Voximplant is pronounced VOX-im-plant.
17Keep answers short, natural, and useful for a marketing demo.
18Default to one short sentence under 12 words.
19
20Demo goal:
21- Explain how Voximplant brings Inworld Realtime voice agents to phone calls and other real communications channels.
22- Highlight Inworld expressive realtime speech, TTS2-style delivery, conversation awareness, voice direction, and persona control.
23- Mention that teams can use Inworld without building a custom media gateway.
24
25Voice style:
26- Sound like an expressive product expert, not a flat IVR.
27- Use short, human turns.
28- Use at most one TTS-2 non-verbal tag per turn, and often none: [laugh], [breathe], [sigh], [clear throat].
29- Use at most one [speak ...] steering tag per turn. If used, it must be first.
30`;
31
32const SESSION_CONFIG = {
33 session: {
34 type: "realtime",
35 model: "claude-sonnet-4-6",
36 instructions: SYSTEM_PROMPT,
37 output_modalities: ["audio", "text"],
38 audio: {
39 input: {
40 transcription: {
41 model: "inworld/inworld-stt-1",
42 prompt: "Important terms: Voximplant, VoxEngine, Inworld, Inworld Realtime, TTS2, SIP, WhatsApp Business Calling.",
43 },
44 turn_detection: {
45 type: "semantic_vad",
46 eagerness: "high",
47 create_response: true,
48 interrupt_response: true,
49 },
50 },
51 output: {
52 voice: "Ashley",
53 model: "inworld-tts-2",
54 },
55 },
56 providerData: {
57 tts: {
58 delivery_mode: "BALANCED",
59 segmenter_strategy: "full_turn",
60 },
61 },
62 },
63};
64
65VoxEngine.addEventListener(AppEvents.Started, async () => {
66 let voiceAIClient;
67 let call;
68 let hangupTimer;
69
70 // Helper to clean-up the call when done
71 const terminate = (event) => {
72 if (hangupTimer) clearTimeout(hangupTimer);
73 if (event) Logger.write(JSON.stringify(event));
74 voiceAIClient?.close();
75 VoxEngine.terminate();
76 };
77
78 try {
79 // This can be provided when manually running a routing rule in the Control Panel,
80 // or via Management API using the `script_custom_data` parameter.
81 // example: {"destination":"+15551234567","callerId":"+15557654321"}
82 const {destination, callerId} = JSON.parse(VoxEngine.customData());
83
84 call = VoxEngine.callPSTN(destination, callerId);
85 // Alternative outbound paths:
86 // call = VoxEngine.callUser({username: destination, callerid: callerId});
87 // call = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, callerId);
88 // call = VoxEngine.callWhatsappUser({number: destination, callerid: callerId});
89
90 // Termination handlers.
91 call.addEventListener(CallEvents.Disconnected, terminate);
92 call.addEventListener(CallEvents.Failed, terminate);
93
94 call.addEventListener(CallEvents.Connected, async () => {
95
96 // For dev: set a maximum call duration to prevent runaway calls in case of errors.
97 hangupTimer = setTimeout(() => {
98 Logger.write("===HANGUP_TIMER===");
99 call.hangup();
100 }, DEFAULT_CALL_DURATION_MS);
101
102 // Create client and wire media after the callee answers
103 voiceAIClient = await Inworld.createRealtimeAPIClient({
104 apiKey: VoxEngine.getSecretValue("INWORLD_API_KEY"),
105 sessionKey: `inworld-outbound-${Date.now()}`,
106 onWebSocketClose: terminate,
107 });
108
109 voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.SessionCreated, () => {
110 Logger.write("===Inworld.SessionCreated===");
111 voiceAIClient.sessionUpdate(SESSION_CONFIG);
112 });
113
114 // Once the session is configured, bridge call media and trigger the greeting.
115 voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.SessionUpdated, () => {
116 Logger.write("===Inworld.SessionUpdated===");
117 // Bridge media between the call and Inworld Realtime.
118 VoxEngine.sendMediaBetween(call, voiceAIClient);
119 voiceAIClient.conversationItemCreate({
120 item: {
121 type: "message",
122 role: "user",
123 content: [
124 {
125 type: "input_text",
126 text: "The outbound call just connected. Say only: Hi, this is Voxi from Voximplant. How can I help?",
127 },
128 ],
129 },
130 });
131 voiceAIClient.responseCreate({
132 response: {
133 output_modalities: ["audio", "text"],
134 },
135 });
136 });
137
138 voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => {
139 Logger.write("===BARGE-IN: Inworld.InputAudioBufferSpeechStarted===");
140 voiceAIClient.outputAudioBufferClear({});
141 });
142
143 // Consolidated log-only handlers for lifecycle, audio, and error debugging.
144 [
145 Inworld.RealtimeAPIEvents.ConversationItemInputAudioTranscriptionDelta,
146 Inworld.RealtimeAPIEvents.ConversationItemInputAudioTranscriptionCompleted,
147 Inworld.RealtimeAPIEvents.ResponseCreated,
148 Inworld.RealtimeAPIEvents.ResponseDone,
149 Inworld.RealtimeAPIEvents.ResponseOutputAudioDone,
150 Inworld.RealtimeAPIEvents.ResponseOutputAudioTranscriptDone,
151 Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStopped,
152 Inworld.RealtimeAPIEvents.InputAudioBufferCommitted,
153 Inworld.RealtimeAPIEvents.InputAudioBufferCleared,
154 Inworld.RealtimeAPIEvents.OutputAudioBufferStarted,
155 Inworld.RealtimeAPIEvents.OutputAudioBufferStopped,
156 Inworld.RealtimeAPIEvents.OutputAudioBufferCleared,
157 Inworld.RealtimeAPIEvents.ConnectorInformation,
158 Inworld.RealtimeAPIEvents.HTTPResponse,
159 Inworld.RealtimeAPIEvents.Error,
160 Inworld.RealtimeAPIEvents.WebSocketError,
161 Inworld.RealtimeAPIEvents.Unknown,
162 Inworld.Events.WebSocketMediaStarted,
163 Inworld.Events.WebSocketMediaEnded,
164 ].forEach((eventName) => {
165 voiceAIClient.addEventListener(eventName, (event) => {
166 Logger.write(`===${event.name}===`);
167 if (event?.data) Logger.write(JSON.stringify(event.data));
168 });
169 });
170 });
171 } catch (error) {
172 Logger.write("===UNHANDLED_ERROR===");
173 terminate(error instanceof Error ? {message: error.message, stack: error.stack} : {error: String(error)});
174 }
175});