> For a complete documentation index, fetch https://docs.voximplant.ai/llms.txt

# Example: Placing an outbound call

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

<blockquote>
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

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](#full-voxengine-scenario).**

## Prerequisites

* Create a [routing rule](/platform/voxengine/routing-rules) 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](/getting-started/network-options/phone-numbers-pstn).
* Store your Inworld API key in Voximplant [Secrets](/platform/voxengine/secrets) under `INWORLD_API_KEY`.

## Outbound call parameters

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

```json title="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`](/api-reference/management-api/scenarios#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:

* [SIP calling options](/getting-started/network-options/sip)
* [Voximplant users calling options](/getting-started/network-options/web-mobile)
* [WhatsApp calling options](/getting-started/network-options/whatsapp)

## 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](inbound) 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:

```js title="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](inbound): 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](inbound).

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

```js title="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](https://docs.inworld.ai/realtime/provider-data).

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:

```js title="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

```javascript title={"voxeengine-inworld-outbound-call.js"} maxLines={0}
/**
 * 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)});
    }
});

```