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
{"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
call.addEventListener(CallEvents.Connected, async () => {
voiceAIClient = await Inworld.createRealtimeAPIClient({
apiKey: VoxEngine.getSecretValue("INWORLD_API_KEY"),
sessionKey: `inworld-outbound-demo-${Date.now()}`,
onWebSocketClose: terminate,
});
});

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
VoxEngine.sendMediaBetween(call, voiceAIClient);
voiceAIClient.conversationItemCreate({
item: {
type: "message",
role: "user",
content: [{
type: "input_text",
text: "The outbound call just connected. Say only: Hi, this is Voxi from Voximplant. How can I help?",
}],
},
});
voiceAIClient.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
voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => {
voiceAIClient.outputAudioBufferClear({});
});

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
/**
* Voximplant + Inworld Realtime API demo
* Scenario: place an outbound call and bridge it to Inworld Realtime.
*
* Configure this in the Voximplant application:
* - Secret `INWORLD_API_KEY` (Voximplant Secrets)
* - Routing rule custom data with `destination` and `callerId`
*/
require(Modules.Inworld);
const DEFAULT_CALL_DURATION_MS = 2 * 60 * 1000;
const SYSTEM_PROMPT = `
You are Voxi, a Voximplant developer advocate on a live outbound phone call.
Voximplant is pronounced VOX-im-plant.
Keep answers short, natural, and useful for a marketing demo.
Default to one short sentence under 12 words.
Demo goal:
- Explain how Voximplant brings Inworld Realtime voice agents to phone calls and other real communications channels.
- Highlight Inworld expressive realtime speech, TTS2-style delivery, conversation awareness, voice direction, and persona control.
- Mention that teams can use Inworld without building a custom media gateway.
Voice style:
- Sound like an expressive product expert, not a flat IVR.
- Use short, human turns.
- Use at most one TTS-2 non-verbal tag per turn, and often none: [laugh], [breathe], [sigh], [clear throat].
- Use at most one [speak ...] steering tag per turn. If used, it must be first.
`;
const SESSION_CONFIG = {
session: {
type: "realtime",
model: "claude-sonnet-4-6",
instructions: SYSTEM_PROMPT,
output_modalities: ["audio", "text"],
audio: {
input: {
transcription: {
model: "inworld/inworld-stt-1",
prompt: "Important terms: Voximplant, VoxEngine, Inworld, Inworld Realtime, TTS2, SIP, WhatsApp Business Calling.",
},
turn_detection: {
type: "semantic_vad",
eagerness: "high",
create_response: true,
interrupt_response: true,
},
},
output: {
voice: "Ashley",
model: "inworld-tts-2",
},
},
providerData: {
tts: {
delivery_mode: "BALANCED",
segmenter_strategy: "full_turn",
},
},
},
};
VoxEngine.addEventListener(AppEvents.Started, async () => {
let voiceAIClient;
let call;
let hangupTimer;
// Helper to clean-up the call when done
const terminate = (event) => {
if (hangupTimer) clearTimeout(hangupTimer);
if (event) Logger.write(JSON.stringify(event));
voiceAIClient?.close();
VoxEngine.terminate();
};
try {
// This can be provided when manually running a routing rule in the Control Panel,
// or via Management API using the `script_custom_data` parameter.
// example: {"destination":"+15551234567","callerId":"+15557654321"}
const {destination, callerId} = JSON.parse(VoxEngine.customData());
call = VoxEngine.callPSTN(destination, callerId);
// Alternative outbound paths:
// call = VoxEngine.callUser({username: destination, callerid: callerId});
// call = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, callerId);
// call = VoxEngine.callWhatsappUser({number: destination, callerid: callerId});
// Termination handlers.
call.addEventListener(CallEvents.Disconnected, terminate);
call.addEventListener(CallEvents.Failed, terminate);
call.addEventListener(CallEvents.Connected, async () => {
// For dev: set a maximum call duration to prevent runaway calls in case of errors.
hangupTimer = setTimeout(() => {
Logger.write("===HANGUP_TIMER===");
call.hangup();
}, DEFAULT_CALL_DURATION_MS);
// Create client and wire media after the callee answers
voiceAIClient = await Inworld.createRealtimeAPIClient({
apiKey: VoxEngine.getSecretValue("INWORLD_API_KEY"),
sessionKey: `inworld-outbound-${Date.now()}`,
onWebSocketClose: terminate,
});
voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.SessionCreated, () => {
Logger.write("===Inworld.SessionCreated===");
voiceAIClient.sessionUpdate(SESSION_CONFIG);
});
// Once the session is configured, bridge call media and trigger the greeting.
voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.SessionUpdated, () => {
Logger.write("===Inworld.SessionUpdated===");
// Bridge media between the call and Inworld Realtime.
VoxEngine.sendMediaBetween(call, voiceAIClient);
voiceAIClient.conversationItemCreate({
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "The outbound call just connected. Say only: Hi, this is Voxi from Voximplant. How can I help?",
},
],
},
});
voiceAIClient.responseCreate({
response: {
output_modalities: ["audio", "text"],
},
});
});
voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => {
Logger.write("===BARGE-IN: Inworld.InputAudioBufferSpeechStarted===");
voiceAIClient.outputAudioBufferClear({});
});
// Consolidated log-only handlers for lifecycle, audio, and error debugging.
[
Inworld.RealtimeAPIEvents.ConversationItemInputAudioTranscriptionDelta,
Inworld.RealtimeAPIEvents.ConversationItemInputAudioTranscriptionCompleted,
Inworld.RealtimeAPIEvents.ResponseCreated,
Inworld.RealtimeAPIEvents.ResponseDone,
Inworld.RealtimeAPIEvents.ResponseOutputAudioDone,
Inworld.RealtimeAPIEvents.ResponseOutputAudioTranscriptDone,
Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStopped,
Inworld.RealtimeAPIEvents.InputAudioBufferCommitted,
Inworld.RealtimeAPIEvents.InputAudioBufferCleared,
Inworld.RealtimeAPIEvents.OutputAudioBufferStarted,
Inworld.RealtimeAPIEvents.OutputAudioBufferStopped,
Inworld.RealtimeAPIEvents.OutputAudioBufferCleared,
Inworld.RealtimeAPIEvents.ConnectorInformation,
Inworld.RealtimeAPIEvents.HTTPResponse,
Inworld.RealtimeAPIEvents.Error,
Inworld.RealtimeAPIEvents.WebSocketError,
Inworld.RealtimeAPIEvents.Unknown,
Inworld.Events.WebSocketMediaStarted,
Inworld.Events.WebSocketMediaEnded,
].forEach((eventName) => {
voiceAIClient.addEventListener(eventName, (event) => {
Logger.write(`===${event.name}===`);
if (event?.data) Logger.write(JSON.stringify(event.data));
});
});
});
} catch (error) {
Logger.write("===UNHANDLED_ERROR===");
terminate(error instanceof Error ? {message: error.message, stack: error.stack} : {error: String(error)});
}
});