*** ## title: 'Example: Placing an outbound call' This example starts a VoxEngine session, places an outbound PSTN call, and bridges audio to Ultravox after the callee answers. **⬇️ Jump to the [Full VoxEngine scenario](#full-voxengine-scenario).** ## Prerequisites * Create a routing rule with this scenario attached (outgoing calls don’t use patterns) so you can run it from the Control Panel or trigger it via API: [https://voximplant.com/docs/getting-started/basic-concepts/routing-rules](https://voximplant.com/docs/getting-started/basic-concepts/routing-rules) * Have a PSTN `destination` number you can dial (E.164 format), for example your mobile number. * Configure a valid outbound `callerId` (for example, a rented Voximplant number or a verified caller ID): [https://voximplant.com/docs/getting-started/basic-concepts/phone-numbers](https://voximplant.com/docs/getting-started/basic-concepts/phone-numbers) * Pass `destination` and `callerId` as routing rule custom data (this example reads them from `VoxEngine.customData()`): `{"destination":"+15551234567","callerId":"+15557654321"}`. * Store your Ultravox API key in Voximplant `ApplicationStorage` under `ULTRAVOX_API_KEY`. * Configure tools (function calling): [https://docs.ultravox.ai/tools/overview](https://docs.ultravox.ai/tools/overview) `VoxEngine.callPSTN(...)` requires a valid callback-capable caller ID (for example, a rented Voximplant number or a verified caller ID). See [https://voximplant.com/docs/getting-started/basic-concepts/phone-numbers](https://voximplant.com/docs/getting-started/basic-concepts/phone-numbers). ## Launch the routing rule For quick testing, you can start this outbound scenario 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** (max 200 bytes) with `destination` and `callerId`: ```json title="Custom data example" {"destination":"+15551234567","callerId":"+15557654321"} ``` For production, start the routing rule via Management API `startScenarios` (pass `rule_id`, and pass the same JSON string in `script_custom_data`): [https://voximplant.com/docs/references/httpapi/scenarios#startscenarios](https://voximplant.com/docs/references/httpapi/scenarios#startscenarios) ## Place the outbound call Outbound calls are placed with `VoxEngine.callPSTN(number, callerid, parameters?)`. In the full example, see `VoxEngine.customData()`, the `destination` / `callerId` parse, and the `AppEvents.Started` handler: ```js title="Place the call" call = VoxEngine.callPSTN(destination, callerId); call.addEventListener(CallEvents.Connected, async () => { // ... }); ``` ## Alternate outbound destinations The example includes **commented‑out** alternatives to place calls to other endpoints. Use these when you want to avoid PSTN or route internally: * **Voximplant users** (`VoxEngine.callUser`): calls another app user inside the same Voximplant application. Best for internal testing or app‑to‑app flows. * **SIP** (`VoxEngine.callSIP`): dial a SIP URI to reach a PBX, carrier, or SIP trunk. Best for enterprise SIP endpoints. * **WhatsApp** (`VoxEngine.callWhatsappUser`): place a WhatsApp Business‑initiated call (requires a WhatsApp Business account and enabled numbers). Relevant guides: * Users: [https://voximplant.com/docs/getting-started/basic-concepts/users](https://voximplant.com/docs/getting-started/basic-concepts/users) * SIP: [https://voximplant.com/docs/guides/calls/sip](https://voximplant.com/docs/guides/calls/sip) * WhatsApp calls: [https://voximplant.com/docs/guides/integrations/whatsapp-calls](https://voximplant.com/docs/guides/integrations/whatsapp-calls) ## Session setup Ultravox uses a WebSocket API client created via `Ultravox.createWebSocketAPIClient(...)`. The key inputs are: * `endpoint`: use `Ultravox.HTTPEndpoint.CREATE_CALL` (or `CREATE_AGENT_CALL`). * `authorizations`: include `X-API-Key` with your Ultravox API key. * `body`: provide `systemPrompt`, `model`, and `voice`. ## Connect call audio For outbound, create the Ultravox client and bridge audio **after the callee answers** (so the agent doesn’t speak into ringback): ```js title="Create the client and bridge audio" voiceAIClient = await Ultravox.createWebSocketAPIClient({ ... }); VoxEngine.sendMediaBetween(call, voiceAIClient); ``` ## Barge-in Clear the output buffer when the caller starts talking to keep the conversation interruption‑friendly: ```js title="Barge-in" voiceAIClient.addEventListener(Ultravox.WebSocketAPIEvents.Transcript, (event) => { const { role } = event?.data?.payload || {}; if (role === "user") voiceAIClient.clearMediaBuffer(); }); ``` ## Full VoxEngine scenario ```javascript title={"voxeengine-ultravox-place-outbound-call.js"} maxLines={0} /** * Voximplant + Ultravox WebSocket API connector demo * Scenario: place an outbound call and bridge it to Ultravox after answer. */ require(Modules.Ultravox); require(Modules.ApplicationStorage); const SYSTEM_PROMPT = `You are a helpful phone assistant for Voximplant callers. Keep responses short and telephony-friendly (1–2 sentences).`; // -------------------- Ultravox WebSocket API settings -------------------- const MODEL = "ultravox-v0.7"; const VOICE_NAME = "Mark"; const DEFAULT_CALL_DURATION_MS = 40000; function parseCustomData() { const raw = VoxEngine.customData(); if (!raw) return {}; try { return JSON.parse(raw); } catch (error) { Logger.write("===CUSTOM_DATA_PARSE_FAILED==="); Logger.write(error); return {}; } } VoxEngine.addEventListener(AppEvents.Started, async () => { let voiceAIClient; const { destination, callerId, callDurationMs = DEFAULT_CALL_DURATION_MS, } = parseCustomData(); if (!destination || !callerId) { Logger.write("===MISSING_CUSTOM_DATA==="); Logger.write(JSON.stringify({destination, callerId})); VoxEngine.terminate(); return; } // Place the outbound call (primary path: PSTN) let call = VoxEngine.callPSTN(destination, callerId); // Alternative outbound paths (uncomment to use): // call = VoxEngine.callUser({ username: "callee", callerid: callerId }); // call = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, callerId); // call = VoxEngine.callWhatsappUser({ number: destination, callerid: callerId }); let hangupTimer; // Termination functions - add cleanup and logging as needed const terminate = () => { if (hangupTimer) clearTimeout(hangupTimer); voiceAIClient?.close(); VoxEngine.terminate(); }; call.addEventListener(CallEvents.Disconnected, terminate); call.addEventListener(CallEvents.Failed, terminate); call.addEventListener(CallEvents.Connected, async () => { hangupTimer = setTimeout(() => call.hangup(), Number(callDurationMs)); const apiKey = (await ApplicationStorage.get("ULTRAVOX_API_KEY")).value; const authorizations = { "X-API-Key": apiKey, }; // Create client and wire media after the callee answers const webSocketAPIClientParameters = { endpoint: Ultravox.HTTPEndpoint.CREATE_CALL, authorizations, body: { systemPrompt: SYSTEM_PROMPT, model: MODEL, voice: VOICE_NAME, }, onWebSocketClose: (event) => { Logger.write("===ULTRAVOX_WEBSOCKET_CLOSED==="); if (event) Logger.write(JSON.stringify(event)); terminate(); }, }; try { voiceAIClient = await Ultravox.createWebSocketAPIClient( webSocketAPIClientParameters, ); VoxEngine.sendMediaBetween(call, voiceAIClient); // ---------------------- Event handlers ----------------------- // Barge-in: keep conversation responsive and capture transcript voiceAIClient.addEventListener( Ultravox.WebSocketAPIEvents.Transcript, (event) => { const payload = event?.data?.payload || event?.data || {}; const role = payload.role; const text = payload.text || payload.delta; if (role && text) Logger.write(`===TRANSCRIPT=== ${role}: ${text}`); if (role === "user") voiceAIClient.clearMediaBuffer(); }, ); voiceAIClient.addEventListener( Ultravox.WebSocketAPIEvents.PlaybackClearBuffer, () => voiceAIClient.clearMediaBuffer(), ); // Consolidated "log-only" handlers - key Ultravox/VoxEngine debugging events [ Ultravox.WebSocketAPIEvents.ConnectorInformation, Ultravox.WebSocketAPIEvents.HTTPResponse, Ultravox.WebSocketAPIEvents.State, Ultravox.WebSocketAPIEvents.Debug, Ultravox.WebSocketAPIEvents.WebSocketError, Ultravox.WebSocketAPIEvents.Unknown, Ultravox.Events.WebSocketMediaStarted, Ultravox.Events.WebSocketMediaEnded, ].forEach((eventName) => { voiceAIClient.addEventListener(eventName, (event) => { Logger.write(`===${event.name}===`); Logger.write(JSON.stringify(event)); }); }); } catch (error) { Logger.write("===SOMETHING_WENT_WRONG==="); Logger.write(error); terminate(); } }); }); ```