# Telnyx Calling: Voice API — Full Documentation > Complete page content for Voice API (Calling section) of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this subsection: https://telnyx.com/llms/calling/voice-api.txt ## Voice API Fundamentals ### Getting Started > Source: https://developers.telnyx.com/docs/voice/programmable-voice/voice-api-fundamentals.md Welcome to the Telnyx Voice API! This guide will walk you through everything you need to start building voice applications with Telnyx, from creating your account to making your first API call. ## What You'll Build In this guide, you'll set up a complete voice application that can make outbound calls and be ready to explore advanced features like AI assistants, speech recognition, and media streaming. ## Prerequisites Before you begin, make sure you have: - A computer with internet access. - Basic understanding of REST APIs and webhooks. - A development environment with your preferred programming language (we'll provide examples in multiple languages). - (Optional) A tool like [ngrok](/development/development-tools/ngrok-setup/index#ngrok) for local webhook testing. ## Create Your Telnyx Account To get started with the Voice API, you'll need a Telnyx account. Follow our [account creation guide](/docs/account-setup/create-account) to set up your account and access the Mission Control Portal. ## Obtain Your API Key To authenticate your Voice API requests, you'll need an API key. Follow our [API key creation guide](/development/api-fundamentals/create-api-keys) to generate and securely store your API key. ## Set Up Your Webhook URL To receive real-time events from the Voice API, you'll need to set up webhooks. Follow our [webhook fundamentals guide](/development/api-fundamentals/webhooks/receiving-webhooks) to configure your webhook URL and create a handler for Voice API events. ## Buy a Phone Number To make calls with the Voice API, you'll need a phone number. Follow our [phone number purchase guide](/docs/numbers/phone-numbers/buy-phone-number) to buy a number that will be associated with your Voice API application. ## Create a Voice API Application A Voice API Application defines how Telnyx handles calls to and from your numbers. ### Creating Your Application 1. In the Mission Control Portal, navigate to **Real-Time Communication** > **Voice** > **Programmable Voice**. 2. Click on the **Create Voice App** button in **Voice API Applications** tab. ![Programmable Voice Section](/img/programmable-voice-section.png) 3. Configure your application and click **Create**. ![Create Voice App](/img/create-voice-app.png) #### Application configuration options - **Application name**: A user-assigned name to help manage the application. - **Webhook URL**: Where Telnyx sends call events, must include a scheme such as 'https'. - **Webhook failover URL**: Backup URL used if primary webhook URL fails after two consecutive delivery attempts. - **Webhook API version**: Determines which webhook format will be used, API v1 or v2 (v2 recommended). - **Anchor site**: Routes media through the site with the lowest round-trip time to your connection. - **Tags**: Create or remove tags associated to this application for organization. - **Enable hang-up on timeout**: Hang up calls if no response to webhook within specified time. - **Custom webhook timeout**: Time in seconds to wait for webhook response before timing out. - **DTMF type**: Touch-tone digit handling method (RFC 2833 recommended). - **Enable call cost**: Receive cost information webhooks for billing and reporting. 4. Configure the **Inbound** settings. ![Configure Inbound](/img/configure-inbound.png) #### Inbound configuration options - **SIP subdomain**: Create a custom SIP address (like `yourname.sip.telnyx.com`) to receive calls from any SIP endpoint. - **SIP subdomain receive settings**: Choose who can call your SIP subdomain - anyone on the internet or only your connections. - **Inbound channel limit**: Set the maximum number of simultaneous inbound calls allowed for this application. - **Enable SHAKEN/STIR headers**: Add call authentication headers to help verify caller identity and reduce spoofing. - **Codecs**: Select which audio and video formats your application will support for optimal call quality. 5. Configure the **Outbound** settings. ![Configure Outbound](/img/configure-outbound.png) #### Outbound configuration options - **Outbound voice profile**: Identifies the associated outbound voice profile for call routing and billing. - **Outbound channel limit**: Sets the maximum number of simultaneous outbound calls allowed for this application. 6. Configure the **Numbers** settings. ![Configure Numbers](/img/configure-numbers.png) #### Numbers configuration This section displays your purchased phone numbers that can be assigned to this Voice API application. You can view number details including status, type (local/toll-free), and purchase date, then select which numbers to associate with your application for handling inbound and outbound calls. 7. Click **Complete**, and congratulations! You just created a Voice API app. It will be listed under your **Voice API Applications** section. ![Voice API Applications List](/img/voice-api-apps-list.png) ## Your First Voice API Call Congratulations! 🎉 You've successfully set up everything needed for your Voice API application. Now comes the exciting part – let's make your first outbound call and bring your application to life! ### Making an Outbound Call Replace the placeholders with your actual values: - `your_api_key`: Your Telnyx API key from the API key section above. - `your_phone_number`: The number you purchased above. - `destination_number`: The number you want to call. - `connection_id`: Your connection_id (which is the Application ID) from your Voice API Application details page. ![Application ID (Connection ID)](/img/application-id-voice-api.png) #### cURL ```bash curl --location 'https://api.telnyx.com/v2/calls' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "to": "+1234567890", "from": "your_phone_number", "connection_id": "your_connection_id", "command_id": "unique-command-id-123" }' ``` #### Node.js ```javascript const axios = require('axios'); const makeCall = async () => { try { const response = await axios.post( 'https://api.telnyx.com/v2/calls', { to: '+1234567890', from: 'your_phone_number', connection_id: 'your_connection_id', }, { headers: { 'Authorization': 'Bearer your_api_key', 'Content-Type': 'application/json' } } ); console.log('Call initiated:', response.data); } catch (error) { console.error('Error:', error.response.data); } }; makeCall(); ``` #### Python ```python import requests url = "https://api.telnyx.com/v2/calls" headers = { "Authorization": "Bearer your_api_key", "Content-Type": "application/json" } data = { "to": "+1234567890", "from": "your_phone_number", "connection_id": "your_connection_id", } response = requests.post(url, json=data, headers=headers) print("Call initiated:", response.json()) ``` ### Understanding the Call Flow When you make a call, here's what happens: 1. **Call Initiated**: Telnyx receives your API request and initiates the call. 2. **Webhook Sent**: Telnyx sends a `call.initiated` webhook to your URL. 3. **Call Progress**: Telnyx sends additional webhooks as the call progresses (`call.answered`, `call.hangup`, etc.). 4. **Your Response**: When the call is answered, you can use Voice API commands to control it (e.g., `speak`, enable `transcription`, start `recording`). 5. **Call End**: Final webhook (`call.hangup`) sent when the call completes. ### Example Webhook Sequence **1. call.initiated** ```json { "created_at": "2025-09-02T09:17:44.019242Z", "event_type": "call.initiated", "payload": { "call_control_id": "v3:RzaeMnE9ebpGCCfKdbNOC_2nU4JJNFMo3rBCpFhCDphE1yP4-2K8UQ", "call_leg_id": "aebb45bc-87dd-11f0-9d4e-02420a1f0b69", "call_session_id": "aeb5639a-87dd-11f0-af54-02420a1f0b69", "client_state": null, "connection_id": "1684641123236054244", "direction": "outgoing", "from": "+12182950349", "occurred_at": "2025-09-02T09:17:43.976123Z", "state": "bridging", "tags": [ "single", "dual" ], "to": "+48661133089" }, "record_type": "event", "webhook_id": "52c959b6-f6a0-4ccc-ad1f-b76fba2efc6d" } ``` **2. call.answered** ```json { "created_at": "2025-09-02T09:17:59.730714Z", "event_type": "call.answered", "payload": { "call_control_id": "v3:RzaeMnE9ebpGCCfKdbNOC_2nU4JJNFMo3rBCpFhCDphE1yP4-2K8UQ", "call_leg_id": "aebb45bc-87dd-11f0-9d4e-02420a1f0b69", "call_session_id": "aeb5639a-87dd-11f0-af54-02420a1f0b69", "client_state": null, "connection_id": "1684641123236054244", "from": "+12182950349", "occurred_at": "2025-09-02T09:17:59.616122Z", "start_time": "2025-09-02T09:17:44.596122Z", "tags": [ "single", "dual" ], "to": "+48661133089" }, "record_type": "event", "webhook_id": "c1d5d77c-349e-4f51-beb6-37b5c263e58a" } ``` **3. call.hangup** ```json { "created_at": "2025-09-02T09:18:06.429625Z", "event_type": "call.hangup", "payload": { "call_control_id": "v3:RzaeMnE9ebpGCCfKdbNOC_2nU4JJNFMo3rBCpFhCDphE1yP4-2K8UQ", "call_leg_id": "aebb45bc-87dd-11f0-9d4e-02420a1f0b69", "call_quality_stats": { "inbound": { "jitter_max_variance": "63.77", "jitter_packet_count": "0", "mos": "4.50", "packet_count": "329", "skip_packet_count": "13" }, "outbound": { "packet_count": "0", "skip_packet_count": "0" } }, "call_session_id": "aeb5639a-87dd-11f0-af54-02420a1f0b69", "client_state": null, "connection_id": "1684641123236054244", "end_time": "2025-09-02T09:18:06.396120Z", "from": "+12182950349", "hangup_cause": "normal_clearing", "hangup_source": "callee", "occurred_at": "2025-09-02T09:18:06.396120Z", "sip_hangup_cause": "200", "start_time": "2025-09-02T09:17:44.596122Z", "tags": [ "single", "dual" ], "to": "+48661133089" }, "record_type": "event", "webhook_id": "30aa187c-cad6-4d29-bd52-e7c792f95313" } ``` ## Testing Your Setup ### Make a Test Call 1. Ensure your webhook handler is running and accessible. 2. Use the API to make an outbound call to your mobile phone. ### Common Issues and Solutions | Issue | Solution | |-------|--| | Webhook not received | Verify URL is publicly accessible, check firewall rules. | | Call immediately ends | Verify the destination number is valid and your Voice API application is properly configured. | | Authentication error | Verify API key is correct and has proper permissions. | | Number not working | Ensure your Telnyx phone number (from) is assigned to your Voice API Application. | ## Record and Retrieve Call Recordings ### Record Calls You can enable call recording for **Outbound Voice Profiles** by configuring your **Record Outbound Calls** settings in the [Mission Control Portal](https://portal.telnyx.com/#/outbound-profiles/). ![Record Outbound Calls](/img/record-calls.png) Alternatively, you can start call recording programmatically using the [Start Recording API](/api-reference/call-commands/recording-start). ### Retrieve Call Recordings You can view and download your call recordings from the [Call Recordings page](https://portal.telnyx.com/#/voice/call-recordings) in the Mission Control Portal. ![Call Recordings Page](/img/retrieve-calls-page.png) ## Next Steps Congratulations! You've successfully set up your first Voice API application. Here are some next steps to enhance your application: ### Explore Advanced Features - **[Voice API Commands & Resources](/docs/voice/programmable-voice/voice-api-commands-and-resources)**: Learn about all available commands like transfer, conference, record, and more. - **[Webhook Handling](/docs/voice/programmable-voice/receiving-webhooks)**: Deep dive into webhook event handling and best practices. - **[Text-to-Speech](/docs/voice/programmable-voice/tts)**: Add natural-sounding voice synthesis to your applications. - **[Speech-to-Text](/docs/voice/programmable-voice/speech-to-text)**: Convert spoken audio into text for voice interactions. - **[AI Assistants](/docs/inference/ai-assistants/no-code-voice-assistant)**: Build intelligent voice assistants with natural conversations. - **[Answering Machine Detection](/docs/voice/programmable-voice/answering-machine-detection)**: Automatically detect and handle voicemail systems. - **[Media Streaming](/docs/voice/programmable-voice/media-streaming)**: Stream real-time audio for advanced processing and analytics. - **[TeXML](/docs/voice/programmable-voice/texml-setup)**: Use Telnyx TeXML to define complex call flows. ### Try Our Tutorials - **[IVR System](/docs/voice/programmable-voice/ivr-demo)**: Build an interactive voice response system. - **[Call Center](/docs/voice/programmable-voice/call-center)**: Create a call center application with queuing. - **[Call Tracking](/docs/voice/programmable-voice/call-tracking)**: Implement call tracking for marketing campaigns. ### Use Our SDKs Speed up development with our official SDKs: - [Node.js SDK](/development/sdk/node). - [Python SDK](/development/sdk/python). - [PHP SDK](/development/sdk/php). - [Ruby SDK](/development/sdk/ruby). - [Java SDK](/development/sdk/java). ## Resources & Support ### Documentation - [Voice API Reference](/api-reference/call-commands/dial): Complete API endpoint documentation. - [WebRTC SDK Documentation](/development/webrtc/fundamentals): Build browser-based calling. - [Migration Guides](/development/migration/call-control-migration-guide): Moving from other providers. ### Getting Help - **[Support Center](https://support.telnyx.com)**: Knowledge base and ticket support. - **[Slack Community](https://joinslack.telnyx.com)**: Connect with developers and Telnyx team. - **[System Status](https://status.telnyx.com)**: Check service availability. - **[GitHub examples](https://github.com/team-telnyx)**: For code samples. --- Ready to build something amazing? You now have all the tools to create powerful voice applications with Telnyx Voice API! --- ### Commands and Resources > Source: https://developers.telnyx.com/docs/voice/programmable-voice/voice-api-commands-and-resources.md The following endpoints can be used with the Voice API applications. ## Call | Endpoint | Description | |------------|-------------| | [/v2/calls](/api-reference/call-commands/dial) | Initiate a new call | | [/v2/calls/:call_control_id/actions/answer](/api-reference/call-commands/answer-call) | Answer an incoming call | | [/v2/calls/:call_control_id/actions/fork_start](/api-reference/call-commands/forking-start) | Start a media fork for a call | | [/v2/calls/:call_control_id/actions/fork_stop](/api-reference/call-commands/forking-stop) | Stop a media fork for a call | | [/v2/calls/:call_control_id/actions/hangup](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup) | Terminate a call | | [/v2/calls/:call_control_id/actions/reject](/api-reference/call-commands/reject-a-call) | Reject an incoming call | | [/v2/calls/:call_control_id/actions/transfer](/api-reference/call-commands/transfer-call) | Transfer a call to another destination | | [/v2/calls/:call_control_id/actions/suppression_start](/api-reference/call-commands/noise-suppression-start-beta) | Start noise suppression for a call | | [/v2/calls/:call_control_id/actions/suppression_stop](/api-reference/call-commands/noise-suppression-stop-beta) | Stop noise suppression for a call | | [/v2/calls/:call_control_id/actions/client_state_update](/api-reference/call-commands/update-client-state) | Update client state information for a call | | [/v2/calls/:call_control_id/actions/bridge](/api-reference/call-commands/bridge-calls) | Bridge a call with another destination | | [/v2/calls/:call_control_id/actions/ai_assistant_start](/api-reference/call-commands/start-ai-assistant) | Start an AI assistant on the call | | [/v2/calls/:call_control_id/actions/ai_assistant_stop](/api-reference/call-commands/stop-ai-assistant) | Stop an AI assistant on the call | | [/v2/calls/:call_control_id/actions/enqueue](/api-reference/call-commands/enqueue-call) | Add a call to a queue | | [/v2/calls/:call_control_id/actions/leave_queue](/api-reference/call-commands/remove-call-from-a-queue) | Remove a call from a queue | | [/v2/calls/:call_control_id/actions/gather_using_audio](/api-reference/call-commands/gather-using-audio) | Play an audio file on the call until the required DTMF signals are gathered to build interactive menus | | [/v2/calls/:call_control_id/actions/gather_using_speak](/api-reference/call-commands/gather-using-speak) | Play a speech on the call until the required DTMF signals are gathered to build interactive menus | | [/v2/calls/:call_control_id/actions/gather_using_ai](/api-reference/call-commands/gather-using-ai) | Collect request information using AI agent| | [/v2/calls/:call_control_id/actions/gather_stop](/api-reference/call-commands/gather-stop) | Stop an ongoing gather operation | | [/v2/calls/:call_control_id/actions/playback_start](/api-reference/call-commands/play-audio-url) | Start playing audio to a call | | [/v2/calls/:call_control_id/actions/playback_stop](/api-reference/call-commands/stop-audio-playback) | Stop playing audio to a call | | [/v2/calls/:call_control_id/actions/record_start](/api-reference/call-commands/recording-start) | Start recording a call | | [/v2/calls/:call_control_id/actions/record_stop](/api-reference/call-commands/recording-stop) | Stop recording a call | | [/v2/calls/:call_control_id/actions/record_pause](/api-reference/call-commands/record-pause#record-pause) | Pause recording a call | | [/v2/calls/:call_control_id/actions/record_resume](/api-reference/call-commands/record-resume#record-resume) | Resume recording a call | | [/v2/calls/:call_control_id/actions/refer](/api-reference/call-commands/sip-refer-a-call) | Send a SIP REFER request for a call | | [/v2/calls/:call_control_id/actions/send_dtmf](/api-reference/call-commands/send-dtmf) | Send DTMF tones to a call | | [/v2/calls/:call_control_id/actions/send_sip_info](/api-reference/call-commands/send-sip-info) | Send a SIP INFO message for a call | | [/v2/calls/:call_control_id/actions/speak](/api-reference/call-commands/speak-text) | Speak text to a call | | [/v2/calls/:call_control_id/actions/streaming_start](/api-reference/call-commands/streaming-start) | Start media streaming for a call | | [/v2/calls/:call_control_id/actions/streaming_stop](/api-reference/call-commands/streaming-stop) | Stop media streaming for a call | | [/v2/calls/:call_control_id/actions/transcription_start](/api-reference/call-commands/transcription-start) | Start transcription for a call | | [/v2/calls/:call_control_id/actions/transcription_stop](/api-reference/call-commands/transcription-stop) | Stop transcription for a call | | [/v2/calls/:call_control_id/actions/siprec_start](/api-reference/call-commands/siprec-start) | Start SIPREC recording for a call | | [/v2/calls/:call_control_id/actions/siprec_stop](/api-reference/call-commands/siprec-stop) | Stop SIPREC recording for a call | | [/v2/connections/:connection_id/active_calls](/api-reference/call-information/list-all-active-calls-for-given-connection#list-all-active-calls-for-given-connection) | List all active calls for given connection | | [/v2/calls/:call_control_id](/api-reference/call-information/retrieve-a-call-status#retrieve-a-call-status) | Retrieve a call status | ## Call events | Endpoint | Description | |------------|-------------| | [/v2/call_events](/api-reference/debugging/list-call-events#list-call-events) | Provide a list of call events based on a filter | ## Conference | Endpoint | Description | |------------|-------------| | [/v2/conferences](/api-reference/conference-commands/list-conferences) | List all conferences | | [/v2/conferences/:id](/api-reference/conference-commands/retrieve-a-conference) | Get details of a specific conference | | [/v2/conferences/:id/participants](/api-reference/conference-commands/list-conference-participants) | List participants in a conference | | [/v2/conferences/:id/participants/:participant_id](/api-reference/conference-commands/update-conference-participant) | Update a participant in a conference | | [/v2/conferences](/api-reference/conference-commands/create-conference) | Create a new conference | | [/v2/conferences/:id/actions/join](/api-reference/conference-commands/join-a-conference) | Join a call to a conference | | [/v2/conferences/:id/actions/leave](/api-reference/conference-commands/leave-a-conference) | Remove a call from a conference | | [/v2/conferences/:id/actions/record_start](/api-reference/conference-commands/conference-recording-start) | Start recording a conference | | [/v2/conferences/:id/actions/record_stop](/api-reference/conference-commands/conference-recording-stop) | Stop recording a conference | | [/v2/conferences/:id/actions/record_pause](/api-reference/conference-commands/conference-recording-pause) | Pause recording a conference | | [/v2/conferences/:id/actions/record_resume](/api-reference/conference-commands/conference-recording-resume) | Resume recording a conference | | [/v2/conferences/:id/actions/mute](/api-reference/conference-commands/mute-conference-participants) | Mute all participants in a conference | | [/v2/conferences/:id/actions/unmute](/api-reference/conference-commands/unmute-conference-participants) | Unmute all participants in a conference | | [/v2/conferences/:id/actions/hold](/api-reference/conference-commands/hold-conference-participants#hold-conference-participants) | Put all participants on hold in a conference | | [/v2/conferences/:id/actions/unhold](/api-reference/conference-commands/unhold-conference-participants#unhold-conference-participants) | Remove hold for all participants in a conference | | [/v2/conferences/:id/actions/play](/api-reference/conference-commands/play-audio-to-conference-participants) | Play audio to a conference | | [/v2/conferences/:id/actions/speak](/api-reference/conference-commands/speak-text-to-conference-participants#speak-text-to-conference-participants) | Speak text to a conference | | [/v2/conferences/:id/actions/stop](/api-reference/conference-commands/stop-audio-being-played-on-the-conference#stop-audio-being-played-on-the-conference) | Stop all ongoing activities in a conference | | [/v2/conferences/:id/actions/update](/api-reference/conference-commands/update-conference-participant) | Update conference participant | ## Connection | Endpoint | Description | |------------|-------------| | [/v2/connections/:connection_id/active_calls](/api-reference/call-information/list-all-active-calls-for-given-connection#list-all-active-calls-for-given-connection) | List active calls for a connection | ## Queue | Endpoint | Description | |------------|-------------| | [/v2/queues/:queue_name](/api-reference/queue-commands/retrieve-a-call-from-a-queue#retrieve-a-call-from-a-queue) | Get details of a specific queue | | [/v2/queues/:queue_name/calls](/api-reference/queue-commands/retrieve-calls-from-a-queue#retrieve-calls-from-a-queue) | List calls in a queue | | [/v2/queues/:queue_name/calls/:call_control_id](/api-reference/queue-commands/retrieve-a-call-from-a-queue#retrieve-a-call-from-a-queue) | Get details of a call in a queue | ## Recording | Endpoint | Description | |------------|-------------| | [/v2/recordings](/api-reference/call-recordings/list-all-call-recordings) | List all recordings | | [/v2/recordings/:id](/api-reference/call-recordings/retrieve-a-call-recording) | Get details of a specific recording | | [/v2/recordings/:id](/api-reference/call-recordings/delete-a-call-recording) | Delete a recording | ## Custom storage | Endpoint | Description | |------------|-------------| | [/v2/custom_storage_credentials](/api-reference/call-recordings/create-a-custom-storage-credential) | Create custom storage credentials | | /v2/custom_storage_credentials | List all custom storage credentials | | [/v2/custom_storage_credentials/:id](/api-reference/call-recordings/retrieve-a-stored-credential#retrieve-a-stored-credential) | Get details of a specific custom storage credential | | [/v2/custom_storage_credentials/:id](/api-reference/call-recordings/delete-a-stored-credential#delete-a-stored-credential) | Delete a custom storage credential | ## Recording transcription | Endpoint | Description | |------------|-------------| | [/v2/recordings/:recording_id/transcriptions](/api-reference/call-recordings/list-all-recording-transcriptions#list-all-recording-transcriptions) | List all transcriptions for a recording | | [/v2/recordings/:recording_id/transcriptions/:id](/api-reference/call-recordings/retrieve-a-recording-transcription#retrieve-a-recording-transcription) | Get details of a specific recording transcription | | [/v2/recordings/:recording_id/transcriptions/:id](/api-reference/texml-rest-commands/delete-a-recording-transcription#delete-a-recording-transcription) | Delete a recording transcription | --- ### Webhooks > Source: https://developers.telnyx.com/docs/voice/programmable-voice/voice-api-webhooks.md ## Overview Voice API webhooks are HTTP callbacks that notify your application in real time when events occur during a call — a call is initiated, audio playback finishes, a recording is saved, and so on. Your application receives a JSON payload for each event and can respond with call control commands to drive the call flow. ## Webhook delivery When an event occurs on a call, Telnyx delivers the webhook to your configured URL. If the primary URL fails, the webhook is sent to the failover URL (if configured). ```mermaid flowchart LR A[Call event occurs] --> B[Deliver to primary URL] B -->|2xx response| C[Success] B -->|No response / error| D{Failover URL configured?} D -->|Yes| E[Deliver to failover URL] D -->|No| F[Delivery failed] E -->|2xx response| C E -->|No response / error| F ``` For details on retry logic, signature verification, and general webhook behavior, see [Webhook Fundamentals](/development/api-fundamentals/webhooks/receiving-webhooks). ## Configuration Webhooks can be configured at three levels: 1. **Connection webhook config** — default webhook URL and settings tied to a [Voice API connection](https://portal.telnyx.com/#/app/connections) in Mission Control. 2. **Custom webhook config** — per-command overrides. Pass `webhook_url` and `webhook_url_method` in any call control command to route that command's webhooks to a different endpoint. 3. **Events webhook config** — advanced configuration that routes specific event types to different URLs. You can also manage webhook settings programmatically via the [Call Control Applications API](/api-reference/call-control-applications/list-call-control-applications). Use [Create](/api-reference/call-control-applications/create-a-call-control-application) or [Update](/api-reference/call-control-applications/update-a-call-control-application) to set webhook URLs, failover URLs, API version, and timeout values on a connection. ### Configuration parameters | Parameter | Type | Description | |-----------|------|-------------| | `webhook_event_url` | String | Primary destination for webhook delivery | | `webhook_event_failover_url` | String | Secondary URL used when the primary fails | | `webhook_api_version` | String | Webhook format version (`"1"` or `"2"`) | | `webhook_timeout_secs` | Integer | Seconds to wait before timing out (0–30, default: null) | ## HTTP methods and headers ### Methods - Webhooks use the `POST` method by default. Pass `webhook_url_method` as `GET` in a call control command to receive that command's webhook payloads as URL query parameters instead of a JSON body. ### Headers Every webhook request includes: | Header | Description | |--------|-------------| | `Content-Type` | `application/json` (POST requests) | | `User-Agent` | `telnyx-webhooks` | | `Telnyx-Signature-Ed25519` | ED25519 signature for [verification](/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing) | | `Telnyx-Timestamp` | Unix timestamp when the webhook was generated | ## Webhook payload structure All Voice API webhooks share a common envelope. Below is an example `call.initiated` payload: ```json { "data": { "record_type": "event", "event_type": "call.initiated", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "d14dbcee-880b-11eb-8204-02420a0f7568", "connection_id": "7267xxxxxxxxxxxxxx", "call_leg_id": "d14dbcee-880b-11eb-8204-02420a0f7568", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "from": "+12025550133", "to": "+12025550131", "direction": "incoming", "state": "parked" } }, "meta": { "attempt": 1, "delivered_to": "https://example.com/webhooks" } } ``` ### Common fields | Field | Location | Description | |-------|----------|-------------| | `record_type` | `data` | Always `"event"` | | `event_type` | `data` | Event name (see [Event types](#event-types) below) | | `id` | `data` | Unique identifier for this webhook event | | `occurred_at` | `data` | ISO 8601 timestamp of when the event occurred | | `call_control_id` | `data.payload` | ID used to issue call control commands for this call leg | | `call_leg_id` | `data.payload` | Unique ID for this call leg — use to correlate webhooks | | `call_session_id` | `data.payload` | Shared ID across related call legs (e.g., both sides of a transfer) | | `connection_id` | `data.payload` | Voice API connection used for the call | | `client_state` | `data.payload` | Base64-encoded state passed through from a previous command | | `from` | `data.payload` | Calling party number or SIP URI | | `to` | `data.payload` | Called party number or SIP URI | | `attempt` | `meta` | Delivery attempt number (increments on retries) | | `delivered_to` | `meta` | URL the webhook was sent to | ## Event types The following event types are fired by the Voice API. Each event type appears in the `event_type` field of the webhook payload. ### Call state | Event | Description | Triggered by | |-------|-------------|--------------| | `call.initiated` | A new call leg has been created | [Dial](/api-reference/call-commands/dial), [Transfer](/api-reference/call-commands/transfer-call), or an inbound call | | `call.answered` | The call has been answered | [Answer](/api-reference/call-commands/answer-call), or remote party picks up | | `call.hold` | The call has been placed on hold | The call is held | | `call.unhold` | The call has been taken off hold | The call is unheld | | `call.hangup` | The call has ended | [Hangup](/api-reference/call-commands/hangup-call), Reject, or remote hangup | | `call.bridged` | Two call legs have been connected | [Bridge](/api-reference/call-commands/bridge-calls), [Transfer](/api-reference/call-commands/transfer-call) | ### Audio playback | Event | Description | Triggered by | |-------|-------------|--------------| | `call.playback.started` | Audio file playback has started | [Play audio](/api-reference/call-commands/play-audio-url), [Gather using audio](/api-reference/call-commands/gather-using-audio) | | `call.playback.ended` | Audio file playback has finished | Playback completes or [Stop playback](/api-reference/call-commands/stop-audio-playback) | | `call.speak.started` | Text-to-speech playback has started | [Speak text](/api-reference/call-commands/speak-text) | | `call.speak.ended` | Text-to-speech playback has finished | Speak completes or [Stop playback](/api-reference/call-commands/stop-audio-playback) | ### DTMF and gather | Event | Description | Triggered by | |-------|-------------|--------------| | `call.dtmf.received` | A DTMF digit was received | Caller presses keypad during [Gather using audio](/api-reference/call-commands/gather-using-audio) or [Gather using speak](/api-reference/call-commands/gather-using-speak) | | `call.gather.ended` | A gather operation has completed | Gather finishes (timeout, max digits, or terminating key) | ### Recording | Event | Description | Triggered by | |-------|-------------|--------------| | `call.recording.saved` | A call recording has been saved | [Recording stop](/api-reference/call-commands/recording-stop), or call ends while recording | ### Answering machine detection (AMD) | Event | Description | Triggered by | |-------|-------------|--------------| | `call.machine.detection.ended` | Standard AMD has determined human vs. machine | [Dial](/api-reference/call-commands/dial) with `answering_machine_detection` enabled | | `call.machine.greeting.ended` | Machine greeting has finished (beep detected) | [Dial](/api-reference/call-commands/dial) with `answering_machine_detection` set to detect greeting end | | `call.machine.premium.detection.ended` | Premium AMD has determined human vs. machine | [Dial](/api-reference/call-commands/dial) with premium AMD enabled | | `call.machine.premium.greeting.ended` | Premium AMD greeting/beep detection completed | [Dial](/api-reference/call-commands/dial) with premium AMD greeting detection | ### Media forking | Event | Description | Triggered by | |-------|-------------|--------------| | `call.fork.started` | Media forking has started | [Forking start](/api-reference/call-commands/forking-start) | | `call.fork.stopped` | Media forking has stopped | [Forking stop](/api-reference/call-commands/forking-stop), or call ends | ### Queue | Event | Description | Triggered by | |-------|-------------|--------------| | `call.enqueued` | Call was placed in a queue | [Enqueue](/api-reference/call-commands/enqueue-call) | | `call.dequeued` | Call was removed from a queue | Dequeue command or call ends | ### Transcription | Event | Description | Triggered by | |-------|-------------|--------------| | `call.transcription` | Real-time transcription data received | [Transcription start](/api-reference/call-commands/transcription-start) | ### Streaming | Event | Description | Triggered by | |-------|-------------|--------------| | `streaming.started` | Media streaming has started | [Streaming start](/api-reference/call-commands/streaming-start) | | `streaming.stopped` | Media streaming has stopped | [Streaming stop](/api-reference/call-commands/streaming-stop), or call ends | ## Response codes Your webhook endpoint's HTTP response determines whether delivery is considered successful: | Code | Meaning | Behavior | |------|---------|----------| | **2xx** | Success | Webhook acknowledged | | **3xx** | Redirect | Followed (up to 3 redirects) | | **408, 429** | Timeout / Rate limited | Retried | | **Other 4xx** | Client error | Not retried | | **5xx** | Server error | Retried | ## Debugging deliveries Use the [Webhook Deliveries API](/api-reference/webhooks/list-webhook-deliveries) to inspect delivery history for your account. You can filter by status, event type, and time range — useful for diagnosing missed or failed webhooks. ```bash # List failed voice webhook deliveries from the last hour curl -X GET "https://api.telnyx.com/v2/webhook_deliveries?filter[status][eq]=failed&filter[event_type]=call.initiated" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Each delivery record includes the full webhook payload, HTTP status codes, and attempt-level details (request/response headers and bodies). See [Get a webhook delivery](/api-reference/webhooks/find-webhook_delivery-details-by-id) for the full response schema. ## Best practices 1. **Return 2xx immediately** — acknowledge receipt within a few seconds, then process asynchronously. 2. **Implement idempotency** — webhooks may be delivered more than once. Use the event `id` to deduplicate. 3. **Verify signatures** — validate the `Telnyx-Signature-Ed25519` header to confirm webhook authenticity. See [Webhook signing](/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing). 4. **Use `command_id`** — include a `command_id` in your call control commands to prevent duplicate command processing. Commands with duplicate IDs within 60 seconds are ignored. 5. **Monitor failures** — track failed webhook deliveries and configure a failover URL for critical applications. --- ### Sending Commands > Source: https://developers.telnyx.com/docs/voice/programmable-voice/sending-commands.md A Voice API command is sent with a `call_control_id`. The `call_control_id` allows a user to communicate to Telnyx the `call_leg` the user wants to control. It also helps Telnyx route the call to the location where the call is being managed, resulting in the lowest possible latency for Call Control interactions. ## Authenticating your Voice API command request Like all other Telnyx API V2 requests, you must authenticate your Voice API command requests by sending the Authorization header with a value of an API Key. You can read more about API Keys [here](/development/api-fundamentals/authentication). Credential Type HTTP Header Format API Key Authorization: Bearer YOUR_API_KEY ## Example: Sending commands with a key + secret To answer the call, send a POST request to the `/actions/answer` endpoint as shown in the example below. *Don't forget to update `YOUR_API_KEY` here.* ```bash curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ https://api.telnyx.com/v2/calls/428c31b6-7af4-4bcb-b7f5-5013ef9657c1/actions/answer ``` After pasting the above content, Kindly check and remove any new line added ## Available commands and their expected Webhooks Telnyx offers a broad range of commands to enable granular control of your call flows. Below are a list of those commands, and the webhooks the Telnyx Voice API platform will always send in response. When multiple webhooks are listed, you can expect to often, though not always, receive webhooks in the order provided. Command Expected Webhooks Answer call call.answered Bridge call call.bridged for Leg A call.bridged for Leg B Dial call.initiated call.answered or call.hangup call.machine.detection.ended - if answering_machine_detection was requested call.machine.greeting.ended - if answering_machine_detection was requested to detect the end of machine greeting Forking start call.fork.started call.fork.stopped Forking stop call.fork.stopped Gather using audio call.playback.started call.playback.ended call.dtmf.received - you may receive many of these webhooks call.gather.ended Gather using speak call.dtmf.received - you may receive many of these webhooks call.gather.ended Hangup call.hangup call.recording.saved - if the call is being recorded Play audio url call.playback.started call.playback.ended Playback stop command call.playback.ended or call.speak.ended Recording start no webhooks Recording stop call.recording.saved Reject call call.hangup Send DTMF no webhooks Speak text call.speak.started call.speak.ended Transfer call call.initiated call.bridged to Leg B call.answered or call.hangup ## Response when sending Voice API commands When you send a Voice API Command, you will immediately receive an http response. Responses include, but are not limited to: HTTP Status Code Message Description 200 OK The request succeeded. 403 Forbidden The request was valid, however the user is not authorized to perform this action. 404 Not Found The requested resource could not be found. 422 Invalid Parameters The request has invalid parameters or the call is no longer active. --- ### Receiving Webhooks > Source: https://developers.telnyx.com/docs/voice/programmable-voice/receiving-webhooks.md When you send a Voice API command and receive a successful response (i.e. 200 OK), you can expect to receive a webhook. The webhook will be delivered to the primary URL specified on the Voice API Application associated with the call. If that URL does not resolve, or your application returns a non 200 OK response, the webhook will be delivered to the failover URL, if one has been specified. In order to minimize webhook delivery time, Telnyx: - does not enforce the order in which webhooks are delivered - retries webhook delivery if your application does not respond within a certain time threshold. As a result, you may encounter: - out-of-order webhooks - simultaneous (or near simultaneous) webhooks - duplicate webhooks Duplicate webhooks may cause your application to issue duplicate commands. You can instruct Telnyx to ignore duplicate commands by sending a `command_id` parameter as part of your commands. Commands with duplicate `command_id`s within 60 seconds will be ignored. Webhooks contain a variety of ID fields which describe them and correlate them with calls. ## Example: Receiving a Webhook When you place an incoming call to a number associated with your Voice API Application, you will receive a callback for the incoming call. It should look something like the JSON below: ```json { "data": { "record_type": "event", "event_type": "call.initiated", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "d14dbcee-880b-11eb-8204-02420a0f7568", "connection_id": "7267xxxxxxxxxxxxxx", "call_leg_id": "d14dbcee-880b-11eb-8204-02420a0f7568", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "from": "+1-202-555-0133", "to": "+12025550131", "direction": "incoming", "state": "parked" } }, "meta": { "attempt": 1, "delivered_to": "http://example.com/webhooks" } } ``` After pasting the above content, Kindly check and remove any new line added Field Value record_type Description of the record. event_type The type of event detected by the Telnyx system id unique id for the webhook occurred_at ISO-8601 datetime of when event occured call_control_id call id used to issue commands via Voice API connection_id Voice API App ID (formerly Telnyx connection ID) used in the call. call_leg_id ID that is unique to the call and can be used to correlate webhook events call_session_id ID that is unique to the call session and can be used to correlate webhook events. Call session is a group of related call legs that logically belong to the same phone call, e.g. an inbound and outbound leg of a transferred call. client_state State received from a command from Number or SIP URI placing the call to Destination number or SIP URI of the call direction Whether the call is 'incoming' or 'outgoing' state Whether the call is in 'bridging' or 'parked' state --- ### Command Retries > Source: https://developers.telnyx.com/docs/voice/programmable-voice/command-retries.md User Applications may encounter the following situations: - 5XX Error: Telnyx actively monitors and alerts on the rate of 500, 501, 503, or 504 errors. - Duplicate Webhooks: Identical webhooks may occasionally be delivered. ## How to use command retries for better reliability Telnyx carefully monitors the Voice API platform for 5XX errors, latency, and duplicate webhooks, and actively works to keep all of these to a minimum. For added reliability, there are several steps developers can take to handle 5XX errors, latency, and duplicate webhooks, and automatically retry commands when such issues are encountered: - `command_id`: send a unique `command_id` parameter as part of your commands. The `command_id` must be unique for each command. We suggest using UUIDv4. - Retry on 5XX Errors: If your application receives a 500 error, immediately retry the command. - Retry on Latency >500ms: If your application fails to receive a HTTP response from Telnyx within 500ms, send an identical command. --- ## Voice Features ### Speech-to-Text > Source: https://developers.telnyx.com/docs/voice/programmable-voice/speech-to-text.md ## Introduction In this tutorial, we will cover how to get a speech-to-text transcription of your calls using Voice API and TeXML. Before starting, please ensure your [Voice API](/docs/voice/programmable-voice/get-started) or [TeXML](/docs/voice/programmable-voice/texml-setup) application is correctly configured. ## Video Tutorial Learn how to implement real-time Speech-to-Text recognition in your voice applications: This video shows how to capture and process spoken input from callers using Telnyx's Speech-to-Text API. ## Supported engines Telnyx offers several speech-to-text engines that can be used to process the audio from the call into a transcription: - Google (default) - Google speech-to-text engine that offers additional features like interim results. - Telnyx - In-house Telnyx speech-to-text engine with significantly better transcription accuracy and lower latency. - Deepgram - Deepgram speech-to-text engine with 3 models (nova-2, nova-3 and flux) that can be set using `transcription_model` setting. - Azure - Azure speech-to-text engine with a strong support for multiple languages and accents. - xAI - xAI Grok STT engine with the `xai/grok-stt` model. - AssemblyAI - AssemblyAI Universal-Streaming engine with the `assemblyai/universal-streaming` model. - Speechmatics - Speechmatics real-time engine with the `speechmatics/standard` model. High accuracy with multilingual and bilingual language packs. - Soniox - Soniox real-time engine with the `soniox/stt-rt-v4` model. Automatic language detection with interim results and endpointing support. ## Voice API The transcription can be enabled for the Voice API calls using [a dedicated endpoint](/api-reference/call-commands/transcription-start) in the following way: *Don't forget to update `YOUR_API_KEY` here.* ```bash curl -i -X POST \ 'https://api.telnyx.com/v2/calls/{call_control_id}/actions/transcription_start' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "language": "en", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "command_id": "891510ac-f3e4-11e8-af5b-de00688a4901", "transcription_engine": "Google/Telnyx/Deepgram/Azure/xAI/AssemblyAI/Speechmatics/Soniox" }' ``` The results are sent as a webhook delivered to the webhook defined for the Voice API application: ```json "data": { "record_type": "event", "event_type": "call.transcription", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:7subYr8fLrXmaAXm8egeAMpoSJ72J3SGPUuome81-hQuaKRf9b7hKA", "call_leg_id": "5ca81340-5beb-11eb-ae45-02420a0f8b69", "call_session_id": "5ca81eee-5beb-11eb-ba6c-02420a0f8b69", "client_state": null, "connection_id": "1240401930086254526", "transcription_data": { "confidence": 0.977219, "is_final": true, "transcript": "hello this is a test speech" } } } ``` ## TeXML You can enable transcription on your TeXML calls by including a `` verb in the TeXML instructions: ```xml ``` The transcription results are sent in the callback in the following format: ```json %{ "AccountSid" : "6d547b4f-993a-4e87-b95c-2d9460b3824b", "CallSid" : "v3:xIscDTsILHoErg5d4BfFWITg7vHmTvTRm-4YEeOgrwESDQsDWQNxvw", "CallSidLegacy" : "v3:xIscDTsILHoErg5d4BfFWITg7vHmTvTRm-4YEeOgrwESDQsDWQNxvw", "Confidence" : "0.9822598695755005", "ConnectionId" : "1614262910593271041", "From" : "+18727726007", "IsFinal" : "true", "To" : "+48664087895", "Transcript" : "let's hear some music" } ``` --- ### Text-to-Speech > Source: https://developers.telnyx.com/docs/voice/programmable-voice/tts.md In this tutorial, you will learn how to get a Text-To-Speech service on your calls using Voice API and TeXML. Before starting, please ensure your [Voice API](https://developers.telnyx.com/docs/voice/programmable-voice/get-started) or [TeXML](https://developers.telnyx.com/docs/voice/programmable-voice/texml-setup) application is correctly configured. ## Video Tutorial Watch this comprehensive video demonstration to see Text-to-Speech features in action: This video demonstrates how to use Telnyx's Text-to-Speech capabilities to create dynamic voice interactions in your applications. ## Telnyx Ultra Telnyx Ultra is a next-generation text-to-speech engine delivering **ultra-quality** voice synthesis with **low latency** and support for **44 languages**. It produces highly natural and expressive speech, making it a great choice for premium voice experiences. You can request Telnyx Ultra voices for your calls using the Voice API: ```shell curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "voice": "Telnyx.Ultra.3e1ed423-17e5-4773-b87c-25b031106e41" }' ``` You can integrate Telnyx Ultra into TeXML scripts using the following format: ```xml The text that should be said on the call! ``` ## Telnyx internal Text-to-Speech engine Telnyx provides a high-quality, low-latency Text-to-Speech (TTS) engine, offering a seamless experience for integrating speech synthesis into your calls. The Telnyx TTS engine ensures a clear and natural-sounding voice, making it an excellent choice for real-time voice applications. You can request Telnyx TTS for your calls using the Voice API. Below is an example of how to trigger speech synthesis with Telnyx TTS: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Telnyx.KokoroTTS.af" }' ``` You can integrate Telnyx TTS into TeXML scripts using the following format: ```bash The text that should be said on the call! ``` With its high-quality voices and low latency, Telnyx TTS is an excellent choice for users seeking to integrate natural-sounding speech into their applications. ## Telnyx Natural Telnyx Natural voices provide enhanced speech quality with improved naturalness and clarity. These voices offer a significant upgrade from basic text-to-speech options, delivering more human-like speech patterns and better pronunciation accuracy. You can request Telnyx Natural voices for your calls using the Voice API: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Telnyx.Natural.abbie" }' ``` You can integrate Telnyx Natural voices into TeXML scripts using the following format: ```xml The text that should be said on the call! ``` ## Telnyx NaturalHD Telnyx NaturalHD voices deliver premium-quality speech synthesis with exceptional clarity and richness. These high-definition voices are ideal for applications where audio quality is critical, such as customer service, media production, or premium user experiences. You can request Telnyx NaturalHD voices for your calls using the Voice API: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Telnyx.NaturalHD.andersen_johan" }' ``` You can integrate Telnyx NaturalHD voices into TeXML scripts using the following format: ```xml The text that should be said on the call! ``` ## AWS Polly Telnyx offers both levels of quality for AWS Polly Text-To-Speech services: neural and standard. The list of voices can be found under the [link](https://docs.aws.amazon.com/polly/latest/dg/available-voices.html). It can be requested on the call using the Voice API command similar to: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Polly.Brian" || "Polly.Amy-Neural" }' ``` It should be used in the following way from TeXML script: ```xml The text that should be said on the call! ``` The neural voice can be used by adding the prefix to the voice name - `Polly.*-Neural` Before you use it, please take a look at the price list under the [link](https://telnyx.com/pricing/call-control). ## Azure AI Speech Telnyx supports Azure AI Speech as a text-to-speech provider. You can find the list of supported voices and languages at the following [link](https://speech.microsoft.com/portal/voicegallery). To use Azure AI Speech, the process is the same as with AWS Polly. Voices should be specified using the following format: Azure.en-CA-ClaraNeural. Azure AI Speech supports two service levels via Telnyx: - Neural - These voices use deep neural networks to generate highly natural and expressive speech. - Ideal for most general applications, they offer high-quality output with support for SSML to customize pronunciation, pitch, rate, and more. - Example: Azure.en-CA-ClaraNeural - Neural HD (High Definition) - HD voices deliver enhanced clarity and richness for scenarios where audio quality is critical—such as media production or premium customer engagement. - These voices provide finer prosody control, improved phonetic detail, and natural pauses, yielding more lifelike speech. - Example: en-US-Emma:DragonHDLatestNeural Here’s an example of using the Telnyx Voice API to synthesize speech with Azure AI Speech: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Azure.en-CA-ClaraNeural" }' ``` The corresponding TeXML script would look like this: ```bash The text that should be said on the call! ``` ## ElevenLabs Users get many voice options with ElevenLabs; however, response latency may exceed what you’d see from AWS Polly or Azure AI Speech. To use the integration, you must provide an API key to your ElevenLabs account. Telnyx offers to store it in a [secure storage](https://developers.telnyx.com/docs/inference/ai-assistants/importing/index#secrets). The API key can be saved in the following way: ```bash curl --location 'https://api.telnyx.com/v2/integration_secrets' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key'' \ --data '{ "identifier":"your_api_key_ref", "value":"api_key" }' ``` The `speak` command should look as follows for the Voice API application: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should said on the call", "voice": "ElevenLabs.Default.cgSgspJ2msm6clMCkdW9", "voice_settings": {"api_key_ref": "your_api_key_ref"} }' ``` A similar effect can be achieved from TeXML using the following script: ```xml The text that should said on the call! ``` *Please note: only a premium ElevenLabs can be used for the integration. The freemium account is not supported* ## MiniMax MiniMax offers high-quality text-to-speech with expressive voices across multiple languages and accents. The `speak` command should look as follows for the Voice API application: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Minimax.speech-2.6-turbo.English_expressive_narrator" }' ``` A similar effect can be achieved from TeXML using the following script: ```xml The text that should be said on the call! ``` ## ResembleAI ResembleAI voices, built on the Chatterbox model, delivering AI voices that preserve emotion, style, and accent for natural sounding delivery. The `speak` command should look as follows for the Voice API application: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Resemble.Pro.Aaron_en-US" }' ``` A similar effect can be achieved from TeXML using the following script: ```xml The text that should be said on the call! ``` ## Inworld Inworld offers expressive multilingual AI voices with two model tiers: Mini and Max. The `speak` command should look as follows for the Voice API application: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Inworld.Mini.Loretta" }' ``` A similar effect can be achieved from TeXML using the following script: ```xml The text that should be said on the call! ``` ## Rime Rime offers two TTS models through Telnyx: - **Coda** (recommended) — Rime's flagship model (May 2026). LLM backbone with dedicated speech engine, sub-100ms latency, 184 voices, and top-rated quality in human evaluations. Supports English, Spanish, French, Portuguese, German, and Japanese. Voices use the `Rime.Coda.` format. - **ArcanaV3** — Previous flagship with multilingual codeswitching across 10 languages: Arabic, English, French, German, Hebrew, Hindi, Japanese, Portuguese, Spanish, and Tamil. Voices use the `Rime.ArcanaV3.` format. Check the [Available Voices](/docs/tts-stt/tts-available-voices) page for specific voice names. The `speak` command should look as follows for the Voice API application: ```bash curl --location 'https://api.telnyx.com/v2/calls/v3:6MytEd1c56mFmXlAziof4tQd-eqOgwQqpFAvECu1gBRrvD5rmsclfg/actions/speak' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer your_api_key' \ --data '{ "payload": "The text that should be said on the call", "voice": "Rime.Coda.cove" }' ``` A similar effect can be achieved from TeXML using the following script: ```xml The text that should be said on the call! ``` --- ### Call Queueing > Source: https://developers.telnyx.com/docs/voice/programmable-voice/queueing-calls.md | [cURL](#curl) | [Python](#python) | --- ## cURL In this tutorial you'll learn how to use the Telnyx Call Queue API to create and manage call queues with just a few API requests. Call Queueing is fully integrated with the Telnyx Voice API, previously called Call Control. This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. ### Adding a call to a new or existing queue A call can be placed into a queue using the [`enqueue` command](/api-reference/call-commands/enqueue-call). Use the `queue_name` parameter to specify a queue into which the call should be placed. - If the `queue_name` refers to a queue that already exists, the call will be placed at the __end__ of the queue. - If the `queue_name` hasn't been used before, a new queue with this name will be created and the call will be placed into it. *Don't forget to update `YOUR_API_KEY` here.* ```bash curl -X POST --header "Content-Type: application/json" --header "Accept: application/json" --header "Authorization: Bearer YOUR_API_KEY" --data '{"queue_name": "support"}' https://api.telnyx.com/v2/calls//actions/enqueue ``` After pasting the above content, Kindly check and remove any new line added > If the call for which the `enqueue` command was issued is bridged to another call leg (i.e. it is in an active conversation with someone) the call will be unbridged. ### Bridging an existing call to a queue The [`bridge` command](/api-reference/call-commands/bridge-calls) can be used to bridge a call to another call waiting in a queue. The queue's `queue_name` should be used as the bridge command's `queue` parameter. For example, a customer support agent can be bridged to the first call from a queue of waiting customer calls. If the customer support agent's active call has a call_control_id `8899ad4a-de6f-11eb-a54c-02420a0d4168` and the support call queue has a name `support`, the command to bridge the call is as follows: ```bash curl -X POST --header "Content-Type: application/json" --header "Accept: application/json" --header "Authorization: Bearer YOUR_API_KEY" --data '{"queue": "support"}' https://api.telnyx.com/v2/calls/8899ad4a-de6f-11eb-a54c-02420a0d4168/actions/bridge ``` After pasting the above content, Kindly check and remove any new line added > When a `bridge` command is issued, the call at the top of the specified queue will be dequeued and a bridge will be attempted. ### Dequeuing calls Calls can be removed from queues in four ways: 1. Ending the call by any means (e.g. a [`hangup` command](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup), or the call being disconnected by calling parties). 2. Issuing any command that results in the call being bridged elsewhere (e.g. [`bridge`](/api-reference/call-commands/bridge-calls), [`transfer`](/api-reference/call-commands/transfer-call), [conference `join`](/api-reference/conference-commands/join-a-conference), [`refer`](/api-reference/call-commands/sip-refer-a-call)). 3. A call is automatically removed from queues if the `max_wait_time_secs` parameter was used when adding the call to a queue and the specified maximum waiting time has elapsed. The automatically dequeued call will remain in a parked state and await further call commands. 4. Sending the [`leave_queue` command](/api-reference/call-commands/remove-call-from-a-queue) with a call's `call_control_id` will remove that call from any queue it is in, leaving it parked awaiting further call commands. ```bash curl -X POST --header "Content-Type: application/json" --header "Accept: application/json" --header "Authorization: Bearer " https://api.telnyx.com/v2/calls//actions/leave_queue ``` After pasting the above content, Kindly check and remove any new line added ### Inspecting queue state There are a number of endpoints that let you inspect your queues and enqueued calls: - [Retrieve a queue](/api-reference/queue-commands/retrieve-a-call-from-a-queue#retrieve-a-call-from-a-queue) - [Retrieve a call from a queue](/api-reference/queue-commands/retrieve-a-call-from-a-queue#retrieve-a-call-from-a-queue) - [List calls in a queue](/api-reference/queue-commands/retrieve-calls-from-a-queue#retrieve-calls-from-a-queue) Empty queues will automatically be garbage collected after a period of inactivity. ### Receiving webhooks for call queueing events The Telnyx API will send you a webhook for every major queue event, i.e. - when a call is put in a queue, - when a call leaves the queue for some reason. Webhooks contain information about the call and the queue with which it is associated. Example webhooks are shown in the API reference for the [`enqueue` command](/api-reference/call-commands/enqueue-call). ### Next steps Now that you've set up simple call queueing functionality with the Telnyx Voice API, why not use call queueing to [build a contact center](/docs/voice/programmable-voice/call-center)? If you're interested in building something more complex or large-scale, our experts are standing by to help. Contact our team today. ## Python In this guide you'll learn how to use the Telnyx Call Queue API to create and manage call queues with just a few API requests. Call Queueing is fully integrated with the Telnyx Voice API, the Telnyx Voice API. This guide assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. ### Adding a call to a new or existing queue A call can be placed into a queue using the [`enqueue` command](/api-reference/call-commands/enqueue-call). Use the `queue_name` parameter to specify a queue into which the call should be placed. - If the `queue_name` refers to a queue that already exists, the call will be placed at the __end__ of the queue. - If the `queue_name` hasn't been used before, a new queue with this name will be created and the call will be placed into it. ```python call.enqueue("tier_1_support") ``` After pasting the above content, Kindly check and remove any new line added > If the call for which the `enqueue` command was issued is bridged to another call leg (i.e. it is in an active conversation with someone) the call will be unbridged. ### Bridging an existing call to a queue The [`bridge` command](/api-reference/call-commands/bridge-calls) can be used to bridge a call to another call waiting in a queue. The queue's `queue_name` should be used as the bridge command's `queue` parameter. For example, a customer support agent can be bridged to the first call from a queue of waiting customer calls. If the customer support agent's active call has a call_control_id `8899ad4a-de6f-11eb-a54c-02420a0d4168` and the support call queue has a name `support`, the command to bridge the call is as follows: ```python call.bridge(call_control_id="bridge_uuid", queue="tier_1_support") ``` After pasting the above content, Kindly check and remove any new line added > When a `bridge` command is issued, the call at the top of the specified queue will be dequeued and a bridge will be attempted. ### Dequeuing calls Calls can be removed from queues in four ways: 1. Ending the call by any means (e.g. a [`hangup` command](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup), or the call being disconnected by calling parties). 2. Issuing any command that results in the call being bridged elsewhere (e.g. [`bridge`](/api-reference/call-commands/bridge-calls), [`transfer`](/api-reference/call-commands/transfer-call), [conference `join`](/api-reference/conference-commands/join-a-conference), [`refer`](/api-reference/call-commands/sip-refer-a-call)). 3. A call is automatically removed from queues if the `max_wait_time_secs` parameter was used when adding the call to a queue and the specified maximum waiting time has elapsed. The automatically dequeued call will remain in a parked state and await further call commands. 4. Sending the [`leave_queue` command](/api-reference/call-commands/remove-call-from-a-queue) with a call's `call_control_id` will remove that call from any queue it is in, leaving it parked awaiting further call commands. ```python call.leave_queue() ``` After pasting the above content, Kindly check and remove any new line added ### Inspecting queue state There are a number of endpoints that let you inspect your queues and enqueued calls: - [Retrieve a queue](/api-reference/queue-commands/retrieve-a-call-from-a-queue#retrieve-a-call-from-a-queue) - [Retrieve a call from a queue](/api-reference/queue-commands/retrieve-a-call-from-a-queue#retrieve-a-call-from-a-queue) - [List calls in a queue](/api-reference/queue-commands/retrieve-calls-from-a-queue#retrieve-calls-from-a-queue) Empty queues will automatically be garbage collected after a period of inactivity. ### Receiving webhooks for call queueing events The Telnyx API will send you a webhook for every major queue event, i.e. - when a call is put in a queue, - when a call leaves the queue for some reason. Webhooks contain information about the call and the queue with which it is associated. Example webhooks are shown in the API reference for the [`enqueue` command](/api-reference/call-commands/enqueue-call). ### Next steps Now that you've set up simple call queueing functionality with the Telnyx Voice API, why not use call queueing to [build a contact center](/docs/voice/programmable-voice/call-center)? If you're interested in building something more complex or large-scale, our experts are standing by to help. Contact our team today. --- ### Answering Machine Detection > Source: https://developers.telnyx.com/docs/voice/programmable-voice/answering-machine-detection.md Outbound calls placed with the Telnyx Voice API can be enabled with Answering Machine Detection (AMD, Voicemail Detection). When a call is answered, Telnyx runs real-time detection to determine if it was picked up by a human or a machine and sends webhooks with the analysis result. ## AMD settings The `answering_machine_detection` value when creating an outbound call or transferring an inbound call can be set to one of the following: Setting Description Webhooks Sent detect Only detect if answering machine or human. call.machine.detection.ended detect_beep Listens for a final "beep" sound after detecting a machine call.machine.detection.ended and call.machine.greeting.ended only if a beep is detected detect_words After a machine is detected, a 30 second long beep detection will begin. Note the answering machine may still be playing it's greeting while the 30 seconds is counting down. call.machine.detection.ended and call.machine.greeting.ended when the beep is detected or at the end of 30 seconds. greeting_end Listens for extended periods of silence or a beep in the greeting to determine if a greeting has ended. call.machine.detection.ended and call.machine.greeting.ended premium RECOMMENDED Premium AMD uses advanced speech recognition technology and machine learning to achieve exceptional accuracy in determining whether a call has been connected to a live person or a machine. call.machine.premium.detection.ended with one of human_residence or human_business or machine or silence or fax_detected or not_sure. If a beep is detected a call.machine.premium.greeting.ended webhook with beep_detected is also sent. If a beep is detected before call.machine.premium.detection.ended, call.machine.premium.greeting.ended is sent. If a beep is detected after call.machine.premium.detection.ended, both webhooks will be sent. premium_ios_call_screening_detection Premium AMD with iOS Call Screening support. Use this when calls may be answered by Apple Call Screening and you need to detect the screening prompt before continuing AMD. call.machine.premium.detection.ended, call.machine.premium.greeting.ended with result=prompt_ended when the screening prompt ends without a beep, and call.machine.premium.call_screening.detected with result=screening when an Apple Call Screening tone is detected. ### Sample dial request ``` POST https://api.telnyx.com/v2/calls HTTP/1.1 Content-Type: application/json; charset=utf-8 Authorization: Bearer YOUR_API_KEY { "connection_id" : "1494404757140276705", "to" : "+19198675309", "from" : "+19842550944", "webhook_url" : "https://webhook_url.com/outbound_call_events", "answering_machine_detection" : "detect_words" } ``` After pasting the above content, Kindly check and remove any new line added ### iOS Call Screening Detection Set `answering_machine_detection` to `premium_ios_call_screening_detection` to run Premium AMD with support for Apple Call Screening. In this mode, Telnyx uses Premium AMD first. If the initial Premium AMD result is `machine`, Telnyx listens for the iOS call-screening prompt to complete or for an Apple Call Screening tone. When an Apple Call Screening tone is detected, Telnyx sends `call.machine.premium.call_screening.detected` with `result=screening` and then restarts Premium AMD on the screened call. If the screening prompt ends without a beep, Telnyx sends `call.machine.premium.greeting.ended` with `result=prompt_ended`. Use this webhook as the signal that your application can provide the response to the iOS screening prompt, such as who is calling and why. Use `answering_machine_detection_config.prompt_end_timeout_millis` to control the maximum amount of time Telnyx waits for the iOS call-screening prompt to end after Premium AMD initially detects a `machine`. The default is `30000` milliseconds. The minimum value is `1000` milliseconds and the maximum value is `120000` milliseconds. #### Sample dial request with iOS Call Screening Detection ```json { "connection_id": "1494404757140276705", "to": "+19198675309", "from": "+19842550944", "webhook_url": "https://webhook_url.com/outbound_call_events", "answering_machine_detection": "premium_ios_call_screening_detection", "answering_machine_detection_config": { "total_analysis_time_millis": 30000, "greeting_duration_millis": 2000, "prompt_end_timeout_millis": 30000 } } ``` #### iOS Call Screening Detection order of operations 1. Create an outbound call or transfer an inbound call with `answering_machine_detection` set to `premium_ios_call_screening_detection`. 2. Receive `call.initiated` webhook. 3. Receive `call.answered` webhook when the call is answered. 4. Receive `call.machine.premium.detection.ended` with the initial Premium AMD result. 5. If the initial result is `machine`, Telnyx waits for the call-screening prompt to end or for an Apple Call Screening tone. 6. If the prompt ends without a beep, receive `call.machine.premium.greeting.ended` with `result=prompt_ended`. After receiving this webhook, your application can provide the response to the iOS screening prompt, such as who is calling and why. 7. If an Apple Call Screening tone is detected, receive `call.machine.premium.call_screening.detected` with `result=screening`. 8. After the screening tone is detected, Telnyx restarts Premium AMD on the screened call. Expect another `call.machine.premium.detection.ended` webhook with the post-screening classification. 9. If the restarted Premium AMD detects a machine and later detects a beep, expect `call.machine.premium.greeting.ended` with `result=beep_detected`. ### General order of operations 1. Create outbound call. 2. Receive `call.initiated` webhook. 3. Receive `call.answered` webhook when the call is answered either by human or machine. 4. Receive `call.machine.detection.ended` webhook with human/machine status. 5. Receive `call.machine.greeting.ended` webhook when beep detected or 30 second timeout. x. **Important** at any point, the callee could hangup generating a `call.hangup` webhook. ## Webhooks ### call.machine.detection.ended The `call.machine.detection.ended` is sent when Telnyx can make a determination on human or machine. The `data.payload.result` will contain the information about the answering machine: Result Description human Human answered call machine Machine answered call not_sure Recommended to treat as if human answered. #### Sample Webhook ```json { "data": { "event_type": "call.machine.detection.ended", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "connection_id": "7267xxxxxxxxxxxxxx", "from": "+35319605860", "result": "machine", "to": "+13129457420" }, "record_type": "event" } } ``` After pasting the above content, Kindly check and remove any new line added ### call.machine.greeting.ended If the `answering_machine_detection` was set to `detect_beep`, `detect_words`, `greeting_end` you could receive a final webhook when the prompt (or beep detection) has finished. The `data.payload.result` will contain the information about the answering machine: Result Description AMD Setting ended Greeting is over. ONLY sent when setting is greeting_end beep_detected Beep has been detected detect_beep and detect_words not_sure 30 second beep detection timeout fired after detecting a machine detect_beep and detect_words #### Sample Webhook ```json { "data": { "event_type": "call.machine.greeting.ended", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "connection_id": "7267xxxxxxxxxxxxxx", "from": "+35319605860", "result": "ended", "to": "+13129457420" }, "record_type": "event" } } ``` After pasting the above content, Kindly check and remove any new line added ## AMD premium Webhooks ### call.machine.premium.detection.ended The `call.machine.premium.detection.ended` webhook is sent when the AMD process can determine whether the call was answered by a `human` or a `machine`. It is possible to specify the number of milliseconds that Telnyx should attempt to perform the detection via the `total_analysis_time_millis` setting. By default, the timeout is set to 30 seconds. If the timeout is reached before the detection is finished, the result in the webhook will be `not_sure`. The `data.payload.result` will contain the information about the answering machine: Result Description human_residence A human answered the call human_business A human answered call machine A machine answered the call silence No sound was detected fax_detected A Fax machine answered the call not_sure Not identifiable, or the configured AMD timeout was reached before the result was available. #### Sample Webhook ```json { "data": { "event_type": "call.machine.premium.detection.ended", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "connection_id": "7267xxxxxxxxxxxxxx", "from": "+35319605860", "result": "machine", "to": "+13129457420" }, "record_type": "event" } } ``` After pasting the above content, Kindly check and remove any new line added ### call.machine.premium.greeting.ended If a machine answered the call, you may receive a final webhook when the beep detection has finished. This webhook is optional and will only be sent if one of two happens: * a beep is detected. In this case, the result is `beep_detected`. * the optional AMD timeout is reached after the call was answered by a machine, but no beep was heard. For this case, the result is `no_beep_detected`. * the iOS call-screening prompt ends without a beep when using `premium_ios_call_screening_detection`. For this case, the result is `prompt_ended`. After receiving this result, your application can provide the response to the iOS screening prompt, such as who is calling and why. The `data.payload.result` will contain the information about the answering machine: Result Description AMD Setting beep_detected Greeting is over. ONLY sent when a machine answered the call, and a beep was heard. no_beep_detected ONLY sent when a machine answered the call, and the AMD timeout was reached before a beep was heard. prompt_ended The iOS call-screening prompt ended without a beep. After receiving this result, your application can provide the response to the iOS screening prompt, such as who is calling and why. ONLY sent when using premium_ios_call_screening_detection. #### Sample Webhook ```json { "data": { "event_type": "call.machine.premium.greeting.ended", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "connection_id": "7267xxxxxxxxxxxxxx", "from": "+35319605860", "result": "beep_detected", "to": "+13129457420" }, "record_type": "event" } } ``` After pasting the above content, Kindly check and remove any new line added ### call.machine.premium.call_screening.detected The `call.machine.premium.call_screening.detected` webhook is sent when `premium_ios_call_screening_detection` detects an Apple Call Screening tone. The `data.payload.result` value is `screening`. After this webhook is sent, Telnyx restarts Premium AMD on the screened call. This webhook is not terminal; expect another `call.machine.premium.detection.ended` webhook with the post-screening classification, and possibly `call.machine.premium.greeting.ended` if a beep is detected after the restart. #### Sample Webhook ```json { "data": { "event_type": "call.machine.premium.call_screening.detected", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "connection_id": "7267xxxxxxxxxxxxxx", "from": "+35319605860", "result": "screening", "to": "+13129457420" }, "record_type": "event" } } ``` After pasting the above content, Kindly check and remove any new line added --- ### Call Recordings Storage > Source: https://developers.telnyx.com/docs/voice/programmable-voice/storing-call-recordings.md ## Overview This tutorial covers how to store your Telnyx call recordings in Amazon S3 or Google Cloud Storage. Call recordings are automatically stored in S3 buckets owned by Telnyx, but users can opt to store recordings in their own S3 or GCS buckets instead. ## Telnyx’s S3 storage The recordings for the calls are stored in the S3 buckets owned by Telnyx. The link to them is shared in the webhook “call.recording.saved” when they are ready to download. ```json { "created_at": "2022-02-04T11:36:13.612978Z", "event_type": "recording_saved", "payload": { "call_control_id": "9977677e-85ae-11ec-826d-02420a0d7e70", "call_leg_id": "9977677e-85ae-11ec-826d-02420a0d7e70", "call_session_id": "99706bb8-85ae-11ec-885c-02420a0d7e70", "channels": "single", "client_state": null, "connection_id": "1684641123236054244", "end_time": "2022-02-04T11:36:12.788447Z", "format": "wav", "occurred_at": "2022-02-04T11:36:13.508869Z", "public_recording_urls": {}, "recording_ended_at": "2022-02-04T11:36:12.788447Z", "recording_id": "10cd86ac-fef8-4765-b203-0f7511f9fc75", "recording_started_at": "2022-02-04T11:36:08.148619Z", "recording_urls": { "wav": "https://s3.amazonaws.com/telephony-recorder-prod/047e057e-cb46-4b11-bb31-37987e753ed7/2022-02-04/9977677e-85ae-11ec-826d-02420a0d7e70-1643974566.wav?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=xxx%2Faws4_request&X-Amz-Date=20220204T113613Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=xxx" }, "start_time": "2022-02-04T11:36:08.148619Z" }, "record_type": "event", "webhook_id": "a6854826-41ae-4781-b299-415a760663a0" } ``` The link to the recording is active for 10 minutes. ## Using custom GCS storage As an alternative, the recordings can be stored in a GCS bucket owned by the customer. To set this up, the following requests with user credentials to the storage need to be sent for every particular application: ```bash curl --location --request POST 'https://api.telnyx.com/v2/custom_storage_credentials/{call_control_application_id}' \ --header 'Authorization: Bearer xxxx' \ --header 'Content-Type: application/json' \ --data-raw '{ "backend": "gcs", "configuration": { "credentials": "JSON_WITH_CREDENTIALS", "bucket": "BUCKET_NAME" } }' ``` The provided user must have permission to store the files in the bucket. The information on how to generate the credentials can be found in the GCS docs here. The information on where the recording is stored will be sent in the webhook “call.recording.saved” as well. ```json { "call_control_id": "d9d877c6-8424-11ec-b0b1-02420a0d7e68", "call_leg_id": "d9d877c6-8424-11ec-b0b1-02420a0d7e68", "call_session_id": "d9d3ba60-8424-11ec-a0c2-02420a0d7e68", "channels": "single", "client_state": null, "connection_id": "1684641123236054244", "end_time": "2022-02-02T12:37:48.670332Z", "format": "wav", "public_recording_urls": {}, "recording_ended_at": "2022-02-02T12:37:48.670332Z", "recording_id": "505f7da3-c50a-4e05-8234-758196993ee3", "recording_started_at": "2022-02-02T12:37:41.996407Z", "recording_urls": { "wav": "gs://tacrde12904/047e057e-cb46-4b11-bb31-37987e753ed7/2022-02-02/d9d877c6-8424-11ec-b0b1-02420a0d7e68-1643805460" }, "start_time": "2022-02-02T12:37:41.996407Z" } ``` ## Using custom S3 storage It is possible to use AWS S3 storage owned by the customer. In that case, the storage configuration can be provided in the following way: ```bash curl --location --request POST 'https://api.telnyx.com/v2/custom_storage_credentials/{call_control_application_id}' \ --header 'Authorization: Bearer xxxx' \ --header 'Content-Type: application/json' \ --data-raw '{ "backend": "s3", "configuration": { "bucket": "BUCKET_NAME", "region" : "REGION_NAME", "aws_access_key_id" : "AWS_ACCESS_KEY_ID", "aws_secret_access_key" : "AWS_SECRET_ACCESS_KEY" } }' ``` In that case, the webhook “call.recording.saved” will look like this: ```json { "call_control_id": "d9d877c6-8424-11ec-b0b1-02420a0d7e68", "call_leg_id": "d9d877c6-8424-11ec-b0b1-02420a0d7e68", "call_session_id": "d9d3ba60-8424-11ec-a0c2-02420a0d7e68", "channels": "single", "client_state": null, "connection_id": "1684641123236054244", "end_time": "2022-02-02T12:37:48.670332Z", "format": "wav", "public_recording_urls": {}, "recording_ended_at": "2022-02-02T12:37:48.670332Z", "recording_id": "505f7da3-c50a-4e05-8234-758196993ee3", "recording_started_at": "2022-02-02T12:37:41.996407Z", "recording_urls": { "wav": "s3://tacrde12904/047e057e-cb46-4b11-bb31-37987e753ed7/2022-02-02/d9d877c6-8424-11ec-b0b1-02420a0d7e68-1643805460" }, "start_time": "2022-02-02T12:37:41.996407Z" } ``` ## Using custom Microsoft Azure Blob Storage storage The Microsoft Azure Blob Storage can be used for storing recordings too. In that case, the storage configuration can be provided in the following way: ```bash curl --location --request POST 'https://api.telnyx.com/v2/custom_storage_credentials/{call_control_application_id}' \ --header 'Authorization: Bearer xxxx' \ --header 'Content-Type: application/json' \ --data-raw '{ "backend": "azure", "configuration": { "bucket": "BUCKET_NAME", "account_name" : "AZURE_ACCOUNT_NAME", "account_key" : "AZURE_ACCOUNT_KEY" } }' ``` In that case, the webhook “call.recording.saved” will look like this: ```json { "call_control_id": "d9d877c6-8424-11ec-b0b1-02420a0d7e68", "call_leg_id": "d9d877c6-8424-11ec-b0b1-02420a0d7e68", "call_session_id": "d9d3ba60-8424-11ec-a0c2-02420a0d7e68", "channels": "single", "client_state": null, "connection_id": "1684641123236054244", "end_time": "2022-02-02T12:37:48.670332Z", "format": "wav", "public_recording_urls": {}, "recording_ended_at": "2022-02-02T12:37:48.670332Z", "recording_id": "505f7da3-c50a-4e05-8234-758196993ee3", "recording_started_at": "2022-02-02T12:37:41.996407Z", "recording_urls": { "wav": "https://my-account.blob.core.windows.net/my-bucket/7ab0a016-479f-4ab1-beaf-87b1a7430a01/2023-12-05/d9d877c6-8424-11ec-b0b1-02420a0d7e68-1643805461" }, "start_time": "2022-02-02T12:37:41.996407Z" } ``` Please find more details see our [call recording API reference](/api-reference/call-recordings/retrieve-a-call-recording). --- ### SIPREC Server Configuration > Source: https://developers.telnyx.com/docs/voice/programmable-voice/siprec-server.md ## Introduction SIPREC (Session Initiation Protocol Recording) is a standardized mechanism for recording VoIP calls. A SIPREC server, also known as a Session Recording Server (SRS), captures and stores these communications for compliance, quality assurance, and other purposes. This guide provides instructions for setting up and configuring a SIPREC server using Telnyx's Voice API connection. ## Setting Up Your SIPREC Server Environment ### Step 1: Create a Voice API Application 1. Log to the [Telnyx portal](https://portal.telnyx.com). 2. Navigate to the Voice -> Programmable Voice -> Voice API section. 3. Create a new Voice API Application by clicking the "Add New Application" button. 4. Configure the application settings as required for your use case. Ensure that you provide a meaningful name and description for easy identification. ### Step 2: Assign an Inbound SIP Subdomain 1. Within your new Voice API Application, locate the section for Inbound Settings. 2. Assign an inbound SIP subdomain to your application. This subdomain will route incoming SIP traffic to your SIPREC server. Example subdomain format: `yourcompany.sip.telnyx.com` 3. Save the changes to apply the new configuration. ![Configure siprec server connection](/img/siprec_server_portal.png) ### Step 3: Configuring the SIPREC Client (SRC) With your Voice API Application and SIP subdomain set up, the next step is configuring your SIPREC Client (SRC) to interact with Telnyx SIPREC Server (SRS). Please configure your SIPREC Client to use the following Session Recording Server URI: ``` sip:username@siprec.telnyx.com;secure=true ``` Where: - **username**: any SIP username can be used, this information will be dropped - **siprec.telnyx.com**: is the Telnyx SIPREC server (SRS) domain **_Note:_** The destination host header should send in the INVITE message as a custom header: `X-DestHost` ### Step 4: Configure SIPREC Token Authentication SIPREC authentication tokens provide an additional layer of security for SIPREC calls directed to your subdomain application. By implementing these tokens, access to your subdomain voice application is restricted and controlled. Tokens can be created via API by updating the connection settings. Use the following sample API call to create tokens: ```bash curl -X PATCH 'https://api.telnyx.com/v2/call_control_applications/:connection_id' \ -H 'content-type: application/json' -H 'authorization: Bearer ' \ --data-raw '{"siprec_tokens": ["test-token1", "test-token2"] }' ``` **_NOTE:_** A maximum of two tokens can be configured per connection. Once tokens are created, all SIPREC INVITEs sent to the subdomain application are authenticated. To enable authentication: 1. Add the token to the SIPREC INVITE request using the `X-Auth-Token` SIP header. 2. During the SIPREC call setup, the token provided in the `X-Auth-Token` header is verified against either of the two configured tokens. Only verified tokens will allow the SIPREC call to proceed. ### Step 5: Initiating the SIPREC call When the siprec session starts, two webhooks, one for each of the SIP calls of the media stream will be sent: ```json { "created_at": "2024-09-17T12:16:39.365600Z", "event_type": "call.initiated", "payload": { "call_control_id": "v3:ehsopsWMfki2clglbX0x4zeJoD1lV52zwLnw7rDJq_-kJoSnZcr0LQ", "call_leg_id": "b0f1c476-74ee-11ef-865a-02420aef3e20", "call_session_id": "b0f1c8fe-74ee-11ef-8704-02420aef3e20", "caller_id_name": "Outbound Call", "client_state": null, "connection_id": "1684641123236054244", "custom_headers": [ { "name": "X-DestHost", "value": "yourcompany" }, { "name": "X-Label", "value": "outbound" }, { "name": "X-ParticipantID", "value": "F7yzLJtvXnTLaZflfmhkwpR5" }, { "name": "X-SIPREC-SessionID", "value": "1772ce22-99ca-476a-b58f-1b848702e2ce" }, { "name": "X-StreamID", "value": "QxjNUZblCzXqwKCTQpsm4Ok3" }, { "name": "X-metadata-xml", "value": "PD94bWwgdmVyc2...." } ], "direction": "incoming", "from": "+48662211095", "occurred_at": "2024-09-17T12:16:39.123397Z", "offered_codecs": "PCMU,PCMA,G729", "start_time": "2024-09-17T12:16:39.123397Z", "state": "parked", "to": "+48662211095" }, "record_type": "event", "webhook_id": "93415bbe-f7c3-4053-8b55-f4a1820d8dbf" } ``` ### Step 5: Recording the call As the next step, the call can be answered and recording can be started using the following Voice API commands: ```bash curl -L 'https://api.telnyx.com/v2/calls/v3:ehsopsWMfki2clglbX0x4zeJoD1lV52zwLnw7rDJq_-kJoSnZcr0LQ/actions/answer' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{}' ``` ```bash curl -L 'https://api.telnyx.com/v2/calls/v3:ehsopsWMfki2clglbX0x4zeJoD1lV52zwLnw7rDJq_-kJoSnZcr0LQ/actions/record_start' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{}' ``` ## SIPREC call flow By following this guide, you should have been able to successfully set up and configure your SIPREC client and server sides. This is a typical SIPREC call flow: ![SIPREC call flow](/img/siprec_call_flow.png) 1. A call is established on the user’s SBC (which has SIPREC SRC capabilities) with RTP streams A and B 2. The SIPREC SRC initiates a SIPREC call towards the Telnyx SIPREC SRS (`siprec.telnyx.com`), with two RTP streams (A and B) 3. The Telnyx SRS initiates two SIP calls towards `sip.telnyx.com`, one for each RTP stream, and each one with `a:sendonly`, indicating that RTP is only sent and not received. 4. Telnyx sends two `call.initiated` webhooks to the Voice API application URL, one for each of the SIP calls, including the `call_control_id` and the metadata from the original SIPREC call (more on this in the next section). 5. The `call_control_id` is used to issue the Voice API commands to answer and record both calls. ## SIPREC metadata Telnyx will pass the metadata that’s included by the SIPREC client on the original SIPREC INVITE message as custom SIP headers on the SIP calls, and each of these will trigger a webhook that contains all of the SIP custom headers. Webhook variables with SIPREC metadata from the INVITE message received by the SIPREC SRS: - to: the content of the SIP URI - SIP Custom headers: any SIP custom headers will be included in the webhook - SIPREC XML default metadata: These variables are extracted from the XML metadata and included in the webhook - DataMode - ParticipantID - NameID-AOR - Associate-Time - StreamID - Label - SIPREC XML custom metadata: any custom variables will also be extracted from the XML metadata and included in the webhook --- ### SIPREC Client > Source: https://developers.telnyx.com/docs/voice/programmable-voice/siprec-client.md ## What is a SIPREC client? SIPREC client (SRC) is a component within the SIPREC framework. The SRC is responsible for initiating and managing the recording session, which communicates to the Session Recording Server (SRS) to send the media streams and metadata for recording. ## Creating a SIPREC server connector To create an SIPREC recording session, you need to define an SIPREC server connector that will be used to establish a connection. It can be done using an API request as follows: ```bash curl --request POST \ --url https://api.telnyx.com/v2/siprec_connectors \ --header 'Authorization: Bearer XXX' \ --header 'Content-Type: application/json' \ --data '{ "name": "siprec-server-connector", "host": "siprec.telnyx.com", "port": 5060 }' ``` ## Creating a SIPREC recording session for Voice API calls To start a SIPREC recording session you can use the following request: ```bash curl --request POST \ --url https://api.telnyx.com/v2/{call_control_id}/actions/siprec_start \ --header 'Accept: application/json' \ --header 'Authorization: Bearer XXX' \ --header 'Content-Type: application/json' \ --data '{ "connector_name": "siprec-server-connector", "direction": "both_tracks" }' ``` The session can be stopped at any point using the `siprec_stop` endpoint: ```bash curl --request POST \ --url https://api.telnyx.com/v2/{call_control_id}/actions/siprec_stop \ --header 'Accept: application/json' \ --header 'Authorization: Bearer XXX' \ --header 'Content-Type: application/json' ``` ## Creating a SIPREC recording session for TeXML calls To initialize the SIPREC recording session the following TeXML instruction can be used: ```xml ``` It can be stopped in the following way: ```xml ``` --- ## AI Capabilities ### SSML Tags > Source: https://developers.telnyx.com/docs/voice/programmable-voice/ssml-tags.md In this tutorial, you'll learn about SSML tags that can help customize your audio response in your text-to-speech application. This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. ## What are SSML tags? Speech Synthesis Markup Language (SSML) is an XML-based markup language that is used to generate synthetic speech for appliations. SSML tags are used to change the tone of speech in the application by adjusting pitching, volume, duration of speech, and more. ## SSML tag examples ### Adding a pause __SSML Tag:__ ` ` There are 2 ways for defining the length of the pause by using the following attributes: 1. Time: Defines the number of s or ms 2. Strength: Chooses the strength using the following values: - None: no pause - Pause: the same duration as after a period - x-weak: the same as none. - weak: sets a pause of the same duration as the pause after a comma - medium: has the same strength as weak - strong: sets a pause of the same duration as the pause after a sentence - x-strong: sets a pause of the same duration as the pause after a paragraph. __Example:__ ``` Mary had a little lamb Whose fleece was white as snow. ``` ### Emphasizing words __SSML Tag:__ ``` ``` The emphasising affects the speed and loudness of reading words and can be defined by using a 'level' attribute with one of the following values: 1. Strong - increases the volume and slows the speaking rate 2. Moderate - increases the volume and slows the speaking rate, but less than Strong 3. Reduced - decreases the volume and speeds up the speaking rate __Example:__ ``` I already told you we're nearly there ``` ### Set a different language __SSML Tag:__ ``` ``` The __xml:lang__ tag defines the language for a specific word or sentence. __Example: __ ``` Puedo hablar español ``` ### Adding a pause between paragraphs __SSML Tag:__ ``` ``` This tag adds a pause between paragraphs that is longer than a regular pause at a comma or at the end of the sentence. __Example:__ ``` This is the first paragraph. This is the second paragraph. ``` ### Using phonetic pronunciation __SSML Tag:__ ``` ``` The phonetic pronunciation requires 2 attributes: 1. __Alphabet__, with the following options: - ipa, meaning the International Phonetic Alphabet (IPA) will be used - x-sampa, which indicates that the Extended Speech Assessment Methods Phonetic Alphabet (X-SAMPA) will be used. 2. __ph__, specifies how the text should be pronounced. __Example:__ ``` Say pronunciation. ``` ### Controlling volume, speaking rate, and pitch __SSML Tag:__: ``` ``` The following attributes can be used with the Prosody tag: 1. __Volume__: - default: resets the volume to default value - silent, x-soft, soft, medium, loud, x-loud: sets the volume to predefined value - +ndB, -ndB: changes the volume relative to the current level 2. __Rate__: - x-slow, slow, medium, fast,x-fast: sets the pitch to a predefined value - n%: a percentage change in speaking pace. __Exmaple:__ ``` Sometimes some words need to be said louder and sometimes a lower volume is a more effective way of interacting with your audience. ``` ### Adding a pause between sentences __SSML Tag:__ ``` ``` This tag adds a pause between lines with the same effect as (.) __Example:__ ``` Here we go round the mulberry bush On a cold and frosty morning ``` ### Controlling how special words are spoken __SSML Tags:__ ``` ``` The say-as tag uses one attribute,'interpret-as', which uses a number of possible available values: - characters or spell-out - cardinal or number - digits - fraction - unit - date - time - address - telephone. __Example:__ ``` +19999999 ``` ### Pronouncing acronyms and abbreviations __SSML Tag:__ ``` ``` This tag should be used with the alias attribute to substitute a different word for selected text such as an acronym or abbreviation. __Example:__ ``` My favorite chemical element is Hg, because it looks so shiny. ``` --- ### Gather Using AI > Source: https://developers.telnyx.com/docs/voice/programmable-voice/gather-using-ai.md ## Introduction Gather using AI is a powerful functionality that allows you to efficiently collect specific information from call participants. By leveraging AI, this feature can gather details such as names, addresses, or other relevant information based on a list you provided. The collected data is then sent back in a structured format. This new AI-driven feature offers a much easier user experience compared to the previous gather functionality, simplifying the process and reducing the time needed to collect information. This guide will walk you through the process of using the 'Gather Using AI' feature effectively. ## Prerequisites The feature can be used for Voice API or TeXML calls similar to regular gather functionality. Please follow the user guides to set up your environment: - [Voice API](https://developers.telnyx.com/docs/voice/programmable-voice/get-started) - [TeXML](https://developers.telnyx.com/docs/voice/programmable-voice/texml-setup) ## Voice API Gather using AI can be enabled for any call by sending the following curl request: ```bash curl --location 'https://api.telnyx.com/v2/calls/{{call_control_id}}/actions/gather_using_ai' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ••••••' \ --data '{ “greeting”: “Can you tell me your age and where you live?“, “parameters”: { “properties”: { “age”: { “description”: “The age of the customer.“, “type”: “integer” }, “location”: { “description”: “The location of the customer.“, “type”: “string” } }, “required”: [ “age”, “location” ], “type”: “object” }, “voice”: “Polly.Brian” }' ``` The `parameters` section contains all the data that you want to gather during the call. Please use the [json schema](https://json-schema.org/) to define them. The `required` section specifies when the `gather` process should end. A webhook will be sent when all values from this list are gathered. If no values are provided, the process will end as soon as the first value is retrieved. ## Message history It is possible to provide the history of the conversation in the `message_history` section, allowing the bot to continue the conversation without losing context ```bash curl --location 'https://api.telnyx.com/v2/calls/{{call_control_id}}/actions/gather_using_ai' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ••••••' \ --data '{ “greeting”: “Can you tell me your age and where you live?“, “parameters”: { “properties”: { “age”: { “description”: “The age of the customer.“, “type”: “integer” }, “location”: { “description”: “The location of the customer.“, “type”: “string” } }, “required”: [ “age”, “location” ], “type”: “object” }, “voice”: “Polly.Brian”, “message_history”: [ { “role”: “assistant”, “content”: “Hello what’s your name?” }, { “role”: “user”, “content”: “My name is Enzo.” } ] }' ``` ## TeXML In the similar, the gather using AI can be enabled from TeXML. There is a dedicated verb `` that can be used for that purpose: ```xml Hello, please provide your age and location. Hello, what's your name? Hi, I'm Enzo. ``` ## Noise suppression The crucial part of the gathering process is to have an accurate transcription of what was said during the call. To improve the quality of the transcription, it is recommended to enable noise suppression for the call. This can be done in the following way for Voice API calls: ```bash curl --request POST \ --url https://api.telnyx.com/v2/calls/${call_control_id}/actions/suppression_start \ --header 'Accept: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY \ --header 'Content-Type: application/json' \ --data '{ "direction": "inbound" }' ``` and in TeXML: ```xml ... ``` ## Need more assistance? If you need some help, [reach out](https://telnyx.com/contact-us) to a member of our team through our form or [the portal](https://portal.telnyx.com/). --- ### Attach an AI Assistant to a Call > Source: https://developers.telnyx.com/docs/voice/programmable-voice/ai-assistant-start.md ## Overview The `ai_assistant_start` command lets you attach a pre-configured AI assistant to an active call. The assistant takes over the conversation, handles speech recognition, and responds using a voice of your choice — no additional infrastructure required. This is different from [Gather using AI](/docs/voice/programmable-voice/gather-using-ai/index), which is purpose-built for collecting structured data. `ai_assistant_start` is for open-ended, conversational AI experiences. ## Prerequisites - A Telnyx account with an active call in progress. Follow the [Voice API getting started guide](/docs/voice/programmable-voice/get-started/index) if you haven't set that up. - An AI assistant. You can create one: - **No-code** via the Portal: [AI Assistants guide](/docs/inference/ai-assistants/no-code-voice-assistant/index) - **Via the API**: [Create an assistant](/api-reference/assistants/create-an-assistant) Once you have an assistant, note its `id` (format: `assistant-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`). ## Start an AI Assistant on a Call Send a `POST` request to `ai_assistant_start` with the `call_control_id` of the active call: ```bash curl -X POST https://api.telnyx.com/v2/calls/{call_control_id}/actions/ai_assistant_start \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "assistant": { "id": "assistant-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } }' ``` That's it. The assistant is now live on the call. ## Webhooks Once started, the assistant emits the following webhooks: | Event | Description | |---|---| | `call.conversation.ended` | The AI conversation has ended | | `call.conversation_insights.generated` | Conversation summary and insights are available | ## Stop the Assistant To stop the assistant and return control to your application: ```bash curl -X POST https://api.telnyx.com/v2/calls/{call_control_id}/actions/ai_assistant_stop \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Add a Participant to an Existing Conversation Once an AI assistant conversation is running, you can bring additional call legs into it using `ai_assistant_join`. For example, you can dial out to a new destination, wait for the person to answer, then add them to the ongoing conversation. ### Prerequisites - An active AI assistant conversation with a known `conversation_id`. The `conversation_id` is returned in the `200` response of `ai_assistant_start`. ### Example: Dial a new participant and add them to the conversation **Step 1 — Dial the new destination:** ```bash curl -X POST https://api.telnyx.com/v2/calls \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connection_id": "your-connection-id", "to": "+15555550100", "from": "+15555550199" }' ``` This returns a new `call_control_id` for the outbound leg. **Step 2 — Wait for the `call.answered` webhook:** When Telnyx sends a `call.answered` event for the new call leg, extract its `call_control_id`. **Step 3 — Add the participant to the conversation:** ### Join the Conversation Once you have the new `call_control_id`: ```bash curl -X POST https://api.telnyx.com/v2/calls/{call_control_id}/actions/ai_assistant_join \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "conversation_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "participant": { "id": "{call_control_id}", "role": "user" } }' ``` The participant's `id` must be the `call_control_id` of the call being added. The only supported `role` is `"user"`. ### Optional Participant Fields | Field | Type | Description | |---|---|---| | `name` | string | Display name for the participant | | `on_hangup` | string | What happens when this participant hangs up: `"continue_conversation"` (default) or `"end_conversation"` | ## Next Steps - Explore the full [AI Assistant API reference](/api-reference/assistants/create-an-assistant) - Configure your assistant in the [Portal](/docs/inference/ai-assistants/no-code-voice-assistant/index) - Collect structured data mid-call with [Gather using AI](/docs/voice/programmable-voice/gather-using-ai/index) --- ### Dialogflow Integration > Source: https://developers.telnyx.com/docs/voice/programmable-voice/dialogflow-es.md Telnyx's Dialogflow tutorial will teach you how to create and manage sophisticated voice interactions with your customers. In this tutorial, you will learn how to integrate your instance of Dialogflow ES, so that you can send the audio from the call to it and get the response from your bot plated on the call. ## Getting started In order to do successfully integrate Dialogflow ES with Telnyx Voice API, you'll need to start by assigning the Dialogflow configuration to your Voice API application using the following update request. ### Dialogflow Connections request sample *Don't forget to update `YOUR_API_KEY` here.* ``` curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{ \ service_account: “GOOGLE_APPLICATION_CREDENTIALS” }' \ https://api.telnyx.com/v2/dialogflow_connections/{connection_id} ``` *Note that GOOGLE_APPLICATION_CREDENTIALS must be provided in a form of encoded json.* Google Dialogflow Setup. When the configuration is assigned to the application users can enable the Dialogflow integration for every outbound or inbound call. ## Enabling dialogflow for outbound calls ``` curl --location --request POST 'https://api.telnyx.com/v2/calls' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "to":"+48662211095", "from":"+13127367481", "connection_id":"1714376719458109299", "enable_dialogflow": true }' ``` ## Enabling dialogflow for inbound calls ``` curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{"enable_dialogflow": true}' \ https://api.telnyx.com/v2/calls/{call_control_id}/actions/answer ``` When the integration is enabled, users can expect the audio provided by Dialogflow will be played on the call, and the following webhooks will be delivered to the webhook url: ``` { "data": { "event_type": "dialogflow.detectintent.response", "id": "22cbf929-9a87-43ab-873b-b832ccf05a05", "occurred_at": "2022-05-23T16:01:53.301413Z", "payload": { "call_control_id": "v2:WE_lbN28P-h81n4xeceODATcx9J-FYci6WO4hP3Gp9Sb789WivnkMw", "call_leg_id": "a3dc0316-dab1-11ec-aaff-02420a0d6669", "call_session_id": "a3cf4428-dab1-11ec-84a9-02420a0d6669", "client_state": null, "confidence": 1, "connection_id": "1669581837548127492", "fulfillment_messages": [{ "text": [ "Hi! I'm the virtual car rental agent. I can help you start a new car rental reservation. How can I assist you today?" ] } ], "is_final": true, "stream_id": "eb724ae3-4f93-49d0-9ab5-c821b47c4fc5", "transcript": "hello" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://webhook.site/e437011a-bb4a-4f34-8060-30e6604c2cf6" } } ``` ## Need some assistance? If you need some help, reach out to a member of our team through our form or the portal. --- ### Real-Time Media Streaming > Source: https://developers.telnyx.com/docs/voice/programmable-voice/media-streaming.md Media Streaming provides instant access to your raw call media. With it, you can deliver exceptional customer experiences—from unlocking new insights through sentiment analysis to providing fast resolutions using AI solutions. You can also bring your own AI engine and use real-time media streaming to connect it directly to call control, enabling custom AI-powered voice applications. When your call is established, Telnyx takes the call media and forks it, so recipients receive the call simultaneously. The Telnyx network ensures that call media can be duplicated, delivered, analyzed, and returned in real-time. The secondary delivery recipient never occupies the call stream, so you never have to worry about degraded quality or dropped connections. This guide covers how to set up media streaming over Websockets. ## Requesting streaming using Dial Command The requesting dial command can be extended in the following way to request streaming using WebSockets: ```bash curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{ "connection_id": "uuid", "to": "+18005550199", "from": "+18005550100", "stream_url": "wss://yourdomain.com", “stream_track”:”inbound_track|outbound_track|both_tracks”}' \ https://api.telnyx.com/v2/calls ``` The following additional attributes need to be added to the request: * stream_url - the destination address when the stream is going to be delivered. * stream_track - specifies which track should be streamed, with possible options: * inbound_track (default) * outbound_track * both_tracks As a response, a regular confirmation is sent: ```json { "data": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "2dc6fc34-f9e0-11ea-b68e-02420a0f7768", "call_session_id": "2dc1b3c8-f9e0-11ea-bc5a-02420a0f7768", "is_alive": false, "record_type": "call" } } ``` ## Requesting streaming using Answer Command Using the same attributes as above, streaming can be requested while answering the call: ```bash curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{ "client_state":"aGF2ZSBhIG5pY2UgZGF5ID1d", "command_id":"891510ac-f3e4-11e8-af5b-de00688a4901", "stream_url": "wss://yourdomain.com", "stream_track":"inbound_track|outbound_track|both_tracks”}' \ https://api.telnyx.com/v2/calls/{call_control_id}/actions/answer ``` In this case, confirmation that the call has been answered includes streaming details: ```json { "data": { "event_type": "call.answered", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-abf3-3bc1-b7f4-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "connection_id": "7267xxxxxxxxxxxxxx", "from": "+35319605860", "state": "answered", "stream_url": "wss://yourdomain.com", "stream_track”:”inbound_track|outbound_track|both_tracks", "to": "+13129457420" }, "record_type": "event" } } ``` ## Streaming process flow When the WebSocket connection is established, the following event is being sent: ```json { "event": "connected", "version": "1.0.0" } ``` Before the stream begins, the streaming.started webhook is sent: ```json { "data": { "event_type": "streaming.started", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "stream_url": "wss://yourdomain.com" }, "record_type": "event" } } ``` An event over WebSockets which contains information in the **mediaFormat section** about the encoding and stream_id that identifies a particular stream: ```json { "event": "start", "sequence_number": "1", "start": { "user_id": "3E6F995F-85F7-4705-9741-53B116D28237", "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "call_session_id": "ff55a038-6f5d-11ef-9692-02420aeffb1f", "from": "+13122010094", "to": "+13122123456", "tags": ["TAG1", "TAG2"], "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "media_format": { "encoding": "PCMU", "sample_rate": 8000, "channels": 1 } }, "stream_id": "32DE0DEA-53CB-4B21-89A4-9E1819C043BC" } ``` The following media events follow the start event: ```json { "event": "media", "sequence_number": "4", "media": { "track": "inbound/outbound", "chunk": "2", "timestamp": "5", "payload": "no+JhoaJjpzSHxAKBgYJDhtEopGKh4aIjZm7JhILBwYIDRg1qZSLh4aIjJevLBUMBwYHDBUsr5eMiIaHi5SpNRgNCAYHCxImu5mNiIaHipGiRBsOCQYGChAf0pyOiYaGiY+e/x4PCQYGCQ4cUp+QioaGiY6bxCIRCgcGCA0ZO6aSi4eGiI2YtSkUCwcGCAwXL6yVjIeGh4yVrC8XDAgGBwsUKbWYjYiGh4uSpjsZDQgGBwoRIsSbjomGhoqQn1IcDgkGBgkPHv+ej4mGhomOnNIfEAoGBgkOG0SikYqHhoiNmbsmEgsHBggNGDWplIuHhoiMl68sFQwHBgcMFSyvl4yIhoeLlKk1GA0IBgcLEia7mY2IhoeKkaJEGw4JBgYKEB/SnI6JhoaJj57/Hg8JBgYJDhxSn5CKhoaJjpvEIhEKBwYIDRk7ppKLh4aIjZi1KRQLBwYIDBcvrJWMh4aHjJWsLxcMCAYHCxQptZiNiIaHi5KmOxkNCAYHChEixJuOiYaGipCfUhwOCQYGCQ8e/56PiYaGiY6c0h8QCgYGCQ4bRKKRioeGiI2ZuyYSCwcGCA0YNamUi4eGiIyXrywVDAcGBwwVLK+XjIiGh4uUqTUYDQgGBwsSJruZjYiGh4qRokQbDgkGBgoQH9KcjomGhomPnv8eDwkGBgkOHFKfkIqGhomOm8QiEQoHBggNGTumkouHhoiNmLUpFAsHBggMFy+slYyHhoeMlawvFwwIBgcLFCm1mI2IhoeLkqY7GQ0IBgcKESLEm46JhoaKkJ9SHA4JBgYJDx7/no+JhoaJjpzSHxAKBgYJDhtEopGKh4aIjZm7JhILBwYIDRg1qZSLh4aIjJevLBUMBwYHDBUsr5eMiIaHi5SpNRgNCAYHCxImu5mNiIaHipGiRBsOCQYGChAf0pyOiYaGiY+e/x4PCQYGCQ4cUp+QioaGiY6bxCIRCgcGCA0ZO6aSi4eGiI2YtSkUCwcGCAwXL6yVjIeGh4yVrC8XDAgGBwsUKbWYjYiGh4uSpjsZDQgGBwoRIsSbjomGhoqQn1Ic" }, "stream_id": "32DE0DEA-53CB-4B21-89A4-9E1819C043BC" } ``` The payload contains a base64-encoded RTP payload (no headers). The order of events is not guaranteed and the chunk number can be used to reorder the events. When the call ends, the **streaming.stopped** webhook is sent: ```json { "data": { "event_type": "streaming.stopped", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2018-02-02T22:25:27.521992Z", "payload": { "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "stream_url": "wss://yourdomain.com" }, "record_type": "event" } } ``` And the stop event over WebSockets connection is sent: ```json { "event": "stop", "sequence_number": "5", "stop": { "user_id": "3E6F995F-85F7-4705-9741-53B116D28237", "call_control_id": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ" }, "stream_id": "32DE0DEA-53CB-4B21-89A4-9E1819C043BC" } ``` Currently only one streaming/fork operation is supported per call. In case of requesting [media forking](/api-reference/call-commands/forking-start) the WebSocket stream will be stopped and replaced by a RTP connection. ## Bidirectional media streaming ### Sending RTP stream The RTP stream can be sent to the call using websocket. The functionality can be enabled by setting `stream_bidirectional_mode` to `rtp`. For `dial` command it should look as follows: ```bash curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{ "connection_id": "uuid", "to": "+18005550199", "from": "+18005550100", "stream_url": "wss://yourdomain.com", "stream_track":"inbound_track|outbound_track|both_tracks"}', "stream_bidirectional_mode":"rtp" \ https://api.telnyx.com/v2/calls ``` It can be requested using answer and streaming_start commands in the same way. The RTP streaming can be sent using media events: ```json { "event": "media", "media": { "payload": "your base64 encoded RTP stream" } } ``` Provided chunks of audio can be in a size of 20 milliseconds to 30 seconds. The number of bidirectional RTP streams per call is limited to 1. #### RTP stream codec There are the following codecs supported by bidirectional streaming: - PCMU, 8 kHz (default) - PCMA, 8 kHz - G722, 8 kHz - OPUS, 8 kHz, 16 kHz - AMR-WB, 8 kHz, 16 kHz - L16, 16 kHz When the audio is sent using a different encoding than on the call, it will be transcoded, which may cause a degradation in quality. The L16 codec provides improved support for AI voice agent integrations by offering reduced latency and eliminating transcoding overhead when interfacing with many AI platforms that natively support linear PCM audio. The codec can be set when the streaming start is requested. For dial command, it looks as follows: ```bash curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{ "connection_id": "uuid", "to": "+18005550199", "from": "+18005550100", "stream_url": "wss://yourdomain.com", "stream_track":"inbound_track|outbound_track|both_tracks"}', "stream_bidirectional_mode": "rtp", "stream_bidirectional_codec": "PCMU|PCMA|G722|OPUS|AMR-WB|L16" \ https://api.telnyx.com/v2/calls ``` ### Sending media files Media files can also be sent back to the call through the websocket. This is done similarly to the playback_start command when using a base64-encoded mp3 file in the payload. Send a packet to the websocket connection as follows: ```json { "event": "media", "media": { "payload" : "your base64 encoded mp3 file" } } ``` The payload, which is a base64-encoded mp3 file, will be played on the call. Multiple media messages will be queued and played in the order they were submitted. Some limitations to be aware of: * Media payloads can only be submitted once per second. * Media must be base64 encoded mp3. ### Clear message Sending a clear message will immediately stop the media playing on the stream and clear the media queue. ```json { "event": "clear" } ``` ### Mark message Mark messages can be used to keep track of media ending on the stream: * You can send a mark message to the stream after a media message. * When the media immediately preceding the mark finishes you will receive the same mark back. Example mark message sent to the stream: ```json { "event": "mark", "mark": { "name": "some_mark_name" } } ``` Example mark message received from the stream: ```json { "event": "mark", "stream_id": "32DE0DEA-53CB-4B21-89A4-9E1819C043BC", "sequence_number": "5", "mark": { "name": "some_mark_name" } } ``` Submitting marks when no audio is played or queued will result in them being immediately sent back. Similarly, the clear message will also result in all queued marks being sent back. ### DTMF message In case of DTMF events on the call, the following message will be sent over websocket: ```json { "event": "dtmf", "stream_id": "32DE0DEA-53CB-4B21-89A4-9E1819C043BC", "occurred_at": "2025-06-05T08:54:19.698408Z", "sequence_number": "5", "dtmf": { "digit": "1" } } ``` `occurred_at` field is a timestamp captured on the Telephony engine side. These are consumed as asynchronous events and in certain circumstances *may* arrive out of order. This field can be used to ensure proper order. ### Error message In case of any case of error during media streaming, an error frame in the following format is sent: ```json { "event": "error", "payload": { "code": integer, "title": string, "detail": string }, "stream_id": uuid } ``` The list of the potential errors: |Code |Title | Description |--------------|--------------------|-----------------------------------------| |100002 | unknown_error | An unknown error occurred on the stream | |100003 | malformed_frame | Received frame was not formed correctly | |100004 | invalid_media | Media provided was not base64 encoded | |100005 | rate_limit_reached | Too many requests | ### Example of integration In the Telnyx GitHub repository there are several examples of integrations with external services using Media streaming: - [Simple application that handles websocket streaming and provides the transcription of it using Node JS](https://github.com/team-telnyx/demo-node-telnyx/tree/master/websocket-demos/websocket) - [Integration with DeepGram transcription engine using Node JS](https://github.com/team-telnyx/demo-node-telnyx/tree/master/websocket-demos/websocket-deepgram-transcription) - [Integration with OpenAI speech-2-speech engine using Node JS](https://github.com/team-telnyx/demo-node-telnyx/tree/master/websocket-demos/websoket-openai-demo) - [Pipecat Telnyx Chatbot in Python](https://github.com/pipecat-ai/pipecat-examples/tree/main/telnyx-chatbot) --- ### Conversation Relay > Source: https://developers.telnyx.com/docs/voice/programmable-voice/conversation-relay.md Conversation Relay connects a live Telnyx call to your WebSocket application. Telnyx handles speech recognition and text-to-speech while your application receives caller input and sends commands back in real time. Use Conversation Relay when you want to build your own conversational voice application, connect calls to an LLM, react to DTMF input, play audio, change languages during a session, or end the relay session from your application. This guide covers how to start Conversation Relay, what WebSocket frames are exchanged, and how to handle callbacks. ## How Conversation Relay works Conversation Relay uses a single bidirectional WebSocket connection per session: 1. Your application provides a public `wss://` WebSocket URL. 2. Telnyx starts Conversation Relay on the call, either from TeXML or with a Programmable Voice command. 3. Telnyx opens a WebSocket connection to your application. 4. Telnyx sends a `setup` frame that identifies the session and call. 5. Telnyx sends `prompt`, `dtmf`, `interrupt`, and `error` frames as call events occur. 6. Your application sends `text`, `play`, `sendDigits`, `language`, or `end` frames back to Telnyx. ## Starting Conversation Relay using TeXML To start Conversation Relay from a TeXML application, return a `` verb with a nested `` verb from your TeXML voice URL. ```xml ``` The `action` attribute is configured on ``, not on ``. It controls where Telnyx sends the action callback after the connected service stops. Your application can use that callback request to return the next TeXML instructions for the call. The following attributes configure the relay session: - url - The WebSocket URL Telnyx connects to. This must start with `ws://` or `wss://`. - welcomeGreeting - Text Telnyx speaks when the relay starts. - voice - The TTS voice used for generated speech. - language - The default language for TTS and transcription. - transcriptionProvider - The speech recognition provider. - dtmfDetection - Enables DTMF detection and `dtmf` WebSocket frames. - interruptible and welcomeGreetingInterruptible - Control when caller input can interrupt speech. Use child nouns when you need per-language settings or custom session data: - Language - Adds a supported language with optional per-language `voice`, `ttsProvider`, `transcriptionProvider`, and `speechModel` settings. - Parameter - Sends custom key-value data to your WebSocket server in the `setup` frame. Point your TeXML application's voice URL to the endpoint that returns this TeXML. When a call reaches that TeXML application, Telnyx fetches the instructions and opens the Conversation Relay WebSocket. ## Starting Conversation Relay using Programmable Voice You can also start Conversation Relay on an active Programmable Voice call with the [Start Conversation Relay command](/api-reference/call-commands/start-conversation-relay). Use this option when your application is already controlling the call through Call Control. After the call is active, send a `conversation_relay_start` command with the call's `call_control_id`. ```bash curl -X POST https://api.telnyx.com/v2/calls/{call_control_id}/actions/conversation_relay_start \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{ "url": "wss://yourdomain.com/conversation-relay", "voice": "Telnyx.Natural.abbie", "tts_provider": "telnyx", "voice_settings": { "type": "telnyx" }, "greeting": "Welcome to the Conversation Relay demo.", "language": "en-US", "languages": [ { "language": "en-US", "tts_provider": "telnyx", "voice": "Telnyx.Natural.abbie", "transcription_engine": "Deepgram", "transcription_engine_config": { "transcription_model": "deepgram/nova-3" } }, { "language": "es", "tts_provider": "telnyx", "voice": "Telnyx.NaturalHD.albion", "transcription_engine": "Deepgram", "transcription_engine_config": { "transcription_model": "deepgram/nova-3" } } ], "dtmf_detection": true, "interruptible": "none", "interruptible_greeting": "none", "transcription_engine": "Deepgram", "transcription_engine_config": { "transcription_model": "deepgram/nova-3" }, "custom_parameters": { "customer_id": "customer_123" } }' ``` The following fields are commonly used: - url - The Conversation Relay WebSocket URL. - greeting - Text Telnyx speaks when the relay starts. - voice - The TTS voice used for generated speech. - tts_provider - The text-to-speech provider. If omitted, Telnyx derives it from `voice` or `provider`. - voice_settings - Provider-specific voice settings. - language - The default language for TTS and transcription. - languages - Per-language TTS and transcription settings. - dtmf_detection - Enables DTMF detection. - interruptible and interruptible_greeting - Control when caller input can interrupt speech. - transcription_engine and transcription_engine_config - Configure the speech recognition provider. - custom_parameters - Key-value data forwarded to the relay session. As a response, Telnyx returns a regular command confirmation with the Conversation Relay session ID: ```json { "data": { "result": "ok", "conversation_relay_id": "d7e9c1d4-8b2a-4b8f-b3a7-9a671c9e9b0a" } } ``` ## WebSocket process flow When Conversation Relay starts, Telnyx opens a WebSocket connection to the configured `url` and sends a `setup` frame: ```json { "type": "setup", "sessionId": "7a7e6a4f-1d44-4f0c-b5d4-9f9bf3a5c1f2", "accountSid": "1f1a8b6f-1234-4abc-9def-1234567890ab", "callSid": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "callControlId": "v2:T02llQxIyaRkhfRKxgAP8nY511EhFLizdvdUKJiSw8d6A9BborherQ", "callSessionId": "ff55a038-6f5d-11ef-9692-02420aeffb1f", "callLegId": "428c31b6-7af4-4b6f-92e7-7a7e6a4f1d44", "from": "+13122010094", "to": "+13122123456", "direction": "inbound", "callerName": "", "callStatus": "active", "customParameters": { "customer_id": "customer_123" } } ``` Use the `setup` frame to initialize call-specific state in your application. After setup, your application can send `text`, `play`, `sendDigits`, `language`, or `end` frames back to Telnyx. Telnyx does not reconnect automatically if the WebSocket closes. Closing the WebSocket terminates the Conversation Relay session. ## Frames sent by Telnyx Telnyx sends the following frame types to your WebSocket server. | Type | Description | |---|---| | `setup` | First frame sent after the WebSocket connects. Identifies the relay session and call. | | `prompt` | Caller speech transcribed to text. Partial transcripts use `last: false`; final transcripts use `last: true`. | | `dtmf` | DTMF digit pressed by the caller. | | `interrupt` | Sent when the caller interrupts ongoing TTS playback. | | `error` | Sent when your application sends an invalid frame or another relay error occurs. | ### Prompt frame Telnyx sends `prompt` frames as the caller speaks: ```json { "type": "prompt", "voicePrompt": "hello there how are you", "lang": "en", "last": true } ``` Use `last: false` prompts as interim transcription updates. Use `last: true` prompts as the final transcript for the caller's utterance. ### DTMF frame When DTMF detection is enabled, keypad input is sent as `dtmf` frames: ```json { "type": "dtmf", "digit": "1" } ``` ### Interrupt frame When the caller barges in over TTS playback, Telnyx sends an `interrupt` frame: ```json { "type": "interrupt", "utteranceUntilInterrupt": "Welcome to Telnyx, how can I help", "durationUntilInterruptMs": 1820 } ``` ## Frames sent by your application Your WebSocket server sends the following frame types to Telnyx. | Type | Description | |---|---| | `text` | Text fragment to speak back to the caller using TTS. | | `play` | Audio URL to play into the call. | | `sendDigits` | DTMF digits to send on the call. | | `language` | Change TTS and/or transcription language during the session. | | `end` | Gracefully end the Conversation Relay session. | ### Sending text Send a `text` frame to speak text to the caller. The text content is sent in the `token` field. ```json { "type": "text", "token": "Hello, how can I help you today?", "last": true } ``` For streaming LLM output, send each token or chunk with `last: false`, then send the final chunk with `last: true`: ```json { "type": "text", "token": "Hello", "last": false } { "type": "text", "token": ", how can I help?", "last": true } ``` If you omit `last`, Telnyx treats it as `false`. Send `last: true` when the turn is complete. ### Playing audio Send a `play` frame to play an audio file by URL: ```json { "type": "play", "source": "https://example.com/audio/welcome.mp3", "loop": 1, "interruptible": true, "preemptible": false } ``` ### Sending DTMF digits Send a `sendDigits` frame to send DTMF digits on the call: ```json { "type": "sendDigits", "digits": "1234#" } ``` Valid characters are `0`-`9`, `A`-`D`, `w` or `W` for a pause, `#`, and `*`. ### Changing language Send a `language` frame to change TTS and/or transcription language: ```json { "type": "language", "ttsLanguage": "es-ES", "transcriptionLanguage": "es-ES" } ``` At least one of `ttsLanguage` or `transcriptionLanguage` must be provided. ### Ending the session Send an `end` frame to end the Conversation Relay session gracefully: ```json { "type": "end", "handoffData": "{\"reason\":\"caller_done\"}" } ``` ## Continuing the call after Conversation Relay The `` verb runs in synchronous mode. When the nested `` service stops, Telnyx either continues with the next TeXML instructions in the same response or, when `action` is set on ``, sends a request to that callback URL so your application can return the next TeXML document. For example, you can provide a follow-up prompt after Conversation Relay ends: ```xml Conversation Relay has ended. Goodbye. ``` In the static example above, the `` and `` instructions are already present after ``. If you set `action`, Telnyx requests the next instructions from your callback URL: ```xml ``` The callback endpoint can then return the next TeXML document dynamically: ```xml Conversation Relay has ended. Goodbye. ``` ## Webhooks When using Programmable Voice, Conversation Relay lifecycle events are delivered to your Call Control webhook URL. When the relay session ends, Telnyx sends: ```json { "data": { "event_type": "call.conversation.ended", "payload": { "reason": "customer_disconnect" }, "record_type": "event" } } ``` If your WebSocket disconnects, the webhook payload `reason` is `customer_disconnect`. ## Error handling If your application sends malformed or invalid frames, Telnyx sends an `error` frame: ```json { "type": "error", "description": "Invalid message: missing required field: token" } ``` After repeated invalid frames, Telnyx can close the WebSocket connection. ## Next steps - Start with `prompt` frames to react to caller speech. - Send `text` frames to stream LLM responses back to the caller. - Use `dtmf` and `sendDigits` frames to integrate keypad-driven flows. - Use `language` frames for multilingual conversations. - Use `end` when your application is ready to leave Conversation Relay. --- ### Noise Suppression > Source: https://developers.telnyx.com/docs/voice/programmable-voice/noise-suppression.md In this tutorial, you'll learn how to enable noise suppression for the Voice API and TeXML calls. Noise suppression works for both AI-powered calls (like AI Assistants and Gather Using AI) and regular voice calls. While it improves audio quality across all call types by reducing background noise, **the biggest value comes from enhanced AI performance**—cleaner audio leads to more accurate speech recognition and better AI responses. This makes noise suppression especially valuable for AI use cases where audio quality directly impacts user experience. ## Voice API The noise suppression can be enabled for the Voice API calls in the following way: *Don't forget to update `YOUR_API_KEY` here.* The only parameter required for the request is `direction` which can have one of the following options: `inbound` | `outbound` | `both`. Please be aware that the charge is applied for each direction separately. ```bash curl --request POST \ --url https://api.telnyx.com/v2/calls/${call_control_id}/actions/suppression_start \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ***' \ --header 'Content-Type: application/json' \ --data '{ "direction": "inbound" }' ``` The noise suppression can be stopped at any time in the following way: ```bash curl --request POST \ --url https://api.telnyx.com/v2/calls/${call_control_id}/actions/suppression_stop \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ***' \ --header 'Content-Type: application/json' \ --data '{}' ``` ## Supported engines Telnyx offers four noise suppression engines, each optimized for different use cases: | Engine | Value | Description | Best for | |--------|-------|-------------|----------| | **Denoiser** | `Denoiser` | Built-in, general-purpose noise reduction | Default option for most calls | | **DeepFilterNet** | `DeepFilterNet` | Open-source, full-band 48 kHz processing | Telephony and WebRTC | | **Krisp** | `Krisp` | Telephony noise suppression with speaker isolation; supports multiple sub-models via `noise_suppression_engine_config.model` | Telephony with speaker isolation | | **AiCoustics** | `AiCoustics` | STT-optimized noise suppression | AI and speech recognition workloads | ### Choosing an engine - For **standard telephony**, use `Denoiser` (default) or `Krisp` for speaker isolation. - For **WebRTC calls**, use `DeepFilterNet` for full-band processing. - For **AI-powered calls** (AI Assistants, Gather Using AI), consider `AiCoustics` for the best speech recognition accuracy. Set the engine using the `noise_suppression_engine` parameter: ```bash curl --request POST \ --url https://api.telnyx.com/v2/calls/${call_control_id}/actions/suppression_start \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ***' \ --header 'Content-Type: application/json' \ --data '{ "direction": "inbound", "noise_suppression_engine": "AiCoustics" }' ``` ## Engine configuration Some engines support additional tuning via `noise_suppression_engine_config`. Parameters are engine-specific and ignored by other engines. ### Krisp models The `Krisp` engine supports three sub-models optimized for different telephony scenarios. Select a model using `noise_suppression_engine_config.model`: | Model value | Best for | |-------------|----------| | `krisp-nlsv-f4t-v2.4ef` | General telephony (default-quality model) | | `krisp-nlsv-f4t-12k-v1.ef` | Narrowband telephony (12 kHz) | | `krisp-nlsv-b1-v1.4ef` | Lightweight model for constrained environments | You can also set the suppression intensity with `suppression_lev` (0–100): ```bash curl --request POST \ --url https://api.telnyx.com/v2/calls/${call_control_id}/actions/suppression_start \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ***' \ --header 'Content-Type: application/json' \ --data '{ "direction": "inbound", "noise_suppression_engine": "Krisp", "noise_suppression_engine_config": { "model": "krisp-nlsv-f4t-12k-v1.ef", "suppression_lev": 80 } }' ``` ### DeepFilterNet configuration The `DeepFilterNet` engine supports two tuning parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `attenuation_lim` | integer (0–100) | `100` | Maximum attenuation applied to noise | | `mode` | `standard` \| `advanced` | — | Processing mode | ### AiCoustics configuration The `AiCoustics` engine exposes enhancement and gain controls: | Parameter | Type | Range | Description | |-----------|------|-------|-------------| | `enhancement_lev` | number | 0–1 | Enhancement intensity | | `voice_gain` | number | 0.1–4 | Voice gain multiplier | ## TeXML In TeXML there is a dedicated verb for enabling the noise suppression on the call. ```xml ... ``` --- ### Deepfake Detection > Source: https://developers.telnyx.com/docs/voice/programmable-voice/deepfake-detection.md Telnyx Deepfake Detection analyzes live call audio to determine whether the remote party's voice is human or AI-generated. When enabled, audio is streamed in real time to a detection model that returns a classification result via webhook. Deepfake detection is available on both **outbound calls** (Dial) and **inbound calls** (Answer). ## How it works 1. You enable `deepfake_detection` when dialing or answering a call. 2. Telnyx streams the remote party's audio to the detection service. 3. The service analyzes audio frames and returns a result within the configured timeout. 4. You receive a `call.deepfake_detection.result` webhook with the classification, or a `call.deepfake_detection.error` webhook if something went wrong. The call proceeds normally while detection runs in the background — there is no impact on call audio or latency. ## Configuration parameters | Parameter | Type | Default | Range | Description | |-----------|------|---------|-------|-------------| | `enabled` | boolean | `false` | — | Whether deepfake detection is enabled. | | `timeout` | integer | `15` | 5–60 | Maximum seconds to wait for a detection result before timing out. | | `rtp_timeout` | integer | `30` | 5–120 | Maximum seconds to wait for RTP audio. If no audio arrives within this window, detection stops with an error. | ## Enabling on an outbound call Include the `deepfake_detection` object when creating an outbound call via the Dial command: ```curl cURL curl -X POST https://api.telnyx.com/v2/calls \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connection_id": "7267xxxxxxxxxxxxxx", "from": "+18005550101", "to": "+18005550100", "deepfake_detection": {"enabled": true} }' ``` ```javascript Node.js import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'] }); const response = await client.calls.dial({ connection_id: '7267xxxxxxxxxxxxxx', from: '+18005550101', to: '+18005550100', deepfake_detection: { enabled: true }, }); console.log(response.data); ``` ```python Python from telnyx import Telnyx client = Telnyx(api_key=os.environ["TELNYX_API_KEY"]) response = client.calls.dial( connection_id="7267xxxxxxxxxxxxxx", from_="+18005550101", to="+18005550100", deepfake_detection={"enabled": True}, ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.calls.dial( connection_id: "7267xxxxxxxxxxxxxx", from: "+18005550101", to: "+18005550100", deepfake_detection: { enabled: true }, ) puts response.data ``` ```java Java import com.telnyx.sdk.*; import com.telnyx.sdk.api.CallCommandsApi; import com.telnyx.sdk.model.*; CallCommandsApi api = new CallCommandsApi(new ApiClient().setApiKey(System.getenv("TELNYX_API_KEY"))); CallRequest req = new CallRequest() .connectionId("7267xxxxxxxxxxxxxx") .from("+18005550101") .to("+18005550100") .deepfakeDetection(new DeepfakeDetection().enabled(true)); api.dial(req); ``` ```go Go import telnyx "github.com/team-telnyx/telnyx-go" client := telnyx.NewClient(os.Getenv("TELNYX_API_KEY")) response, _ := client.Calls.Dial(ctx, &telnyx.CallDialParams{ ConnectionID: "7267xxxxxxxxxxxxxx", From: "+18005550101", To: "+18005550100", DeepfakeDetection: &telnyx.DeepfakeDetection{Enabled: true}, }) fmt.Println(response) ``` ```php PHP $telnyx = new \Telnyx\Client(getenv('TELNYX_API_KEY')); $response = $telnyx->calls->dial([ 'connection_id' => '7267xxxxxxxxxxxxxx', 'from' => '+18005550101', 'to' => '+18005550100', 'deepfake_detection' => ['enabled' => true], ]); echo $response->data; ``` ## Enabling on an inbound call Add `deepfake_detection` to the Answer command when picking up an incoming call: ```curl cURL curl -X POST https://api.telnyx.com/v2/calls/$CALL_CONTROL_ID/actions/answer \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"deepfake_detection": {"enabled": true}}' ``` ```javascript Node.js import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'] }); const response = await client.calls.answer(callControlId, { deepfake_detection: { enabled: true }, }); console.log(response.data); ``` ```python Python from telnyx import Telnyx client = Telnyx(api_key=os.environ["TELNYX_API_KEY"]) response = client.calls.answer( call_control_id, deepfake_detection={"enabled": True}, ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.calls.answer( call_control_id, deepfake_detection: { enabled: true }, ) puts response.data ``` ```java Java import com.telnyx.sdk.*; import com.telnyx.sdk.api.CallCommandsApi; import com.telnyx.sdk.model.*; CallCommandsApi api = new CallCommandsApi(new ApiClient().setApiKey(System.getenv("TELNYX_API_KEY"))); AnswerRequest req = new AnswerRequest() .deepfakeDetection(new DeepfakeDetection().enabled(true)); api.answer(callControlId, req); ``` ```go Go import telnyx "github.com/team-telnyx/telnyx-go" client := telnyx.NewClient(os.Getenv("TELNYX_API_KEY")) response, _ := client.Calls.Answer(ctx, callControlID, &telnyx.CallAnswerParams{ DeepfakeDetection: &telnyx.DeepfakeDetection{Enabled: true}, }) fmt.Println(response) ``` ```php PHP $telnyx = new \Telnyx\Client(getenv('TELNYX_API_KEY')); $response = $telnyx->calls->answer($callControlId, [ 'deepfake_detection' => ['enabled' => true], ]); echo $response->data; ``` ## Handling the result webhook When detection completes, you receive a `call.deepfake_detection.result` webhook: ```json { "record_type": "event", "event_type": "call.deepfake_detection.result", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2025-06-15T14:30:27.521992Z", "payload": { "call_control_id": "v3:MdI91X4lWFEs7IgbBEOT9M4AigoY08M0WWZFISt1Yw2axZ_IiE4pqg", "connection_id": "7267xxxxxxxxxxxxxx", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "result": "fake", "score": 0.87, "consistency": 94.5 } } ``` ### Result fields | Field | Type | Description | |-------|------|-------------| | `result` | string | `real` — human voice detected. `fake` — AI-generated voice detected. `silence_timeout` — no analyzable speech before timeout. | | `score` | float \| null | Probability the audio is AI-generated, from `0.0` (likely real) to `1.0` (likely deepfake). Null for `silence_timeout`. | | `consistency` | float \| null | Percentage (0–100) indicating how consistently the model classified the audio across frames. Values above 90% indicate high confidence. Null for `silence_timeout`. | ## Handling errors If detection fails, you receive a `call.deepfake_detection.error` webhook: ```json { "record_type": "event", "event_type": "call.deepfake_detection.error", "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0", "occurred_at": "2025-06-15T14:30:27.521992Z", "payload": { "call_control_id": "v3:MdI91X4lWFEs7IgbBEOT9M4AigoY08M0WWZFISt1Yw2axZ_IiE4pqg", "connection_id": "7267xxxxxxxxxxxxxx", "call_leg_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "call_session_id": "428c31b6-7af4-4bcb-b7f5-5013ef9657c1", "client_state": "aGF2ZSBhIG5pY2UgZGF5ID1d", "error_message": "detection_timeout" } } ``` ### Error types | Error | Description | |-------|-------------| | `detection_timeout` | No detection result received within the configured `timeout`. | | `rtp_timeout` | No RTP audio received within the configured `rtp_timeout`. | | `dfd_connection_error` | Could not connect to the detection service. | | `dfd_stream_error` | Audio stream to the detection service failed. | ## Example: screening inbound calls This example webhook server answers inbound calls with deepfake detection enabled and takes action based on the result. ```javascript Node.js const express = require("express"); const Telnyx = require("telnyx"); const app = express(); app.use(express.json()); const client = new Telnyx({ apiKey: process.env.TELNYX_API_KEY, }); // Answer incoming calls with deepfake detection enabled app.post("/webhooks/call-initiated", async (req, res) => { const { call_control_id } = req.body.payload; await client.calls.actions.answer(call_control_id, { deepfake_detection: { enabled: true, timeout: 15, rtp_timeout: 30, }, }); res.sendStatus(200); }); // Handle deepfake detection results app.post("/webhooks/deepfake-result", async (req, res) => { const { call_control_id, result, score, consistency } = req.body.payload; console.log( `Detection result: ${result}, score: ${score}, consistency: ${consistency}%` ); if (result === "fake" && score > 0.8) { // High-confidence deepfake — hang up or route to fraud queue await client.calls.actions.hangup(call_control_id, {}); console.log("Deepfake detected — call terminated."); } else if (result === "real") { // Human caller — proceed normally console.log("Human caller verified."); } // silence_timeout — caller didn't speak; handle as needed res.sendStatus(200); }); // Handle detection errors gracefully app.post("/webhooks/deepfake-error", async (req, res) => { const { call_control_id, error_message } = req.body.payload; console.log(`Deepfake detection error: ${error_message}`); // Continue the call normally if detection fails res.sendStatus(200); }); app.listen(3000, () => console.log("Webhook server running on port 3000")); ``` ```python Python import os from flask import Flask, request from telnyx import Telnyx app = Flask(__name__) client = Telnyx(api_key=os.environ["TELNYX_API_KEY"]) @app.route("/webhooks/call-initiated", methods=["POST"]) def call_initiated(): call_control_id = request.json["payload"]["call_control_id"] client.calls.actions.answer( call_control_id, deepfake_detection={ "enabled": True, "timeout": 15, "rtp_timeout": 30, }, ) return "", 200 @app.route("/webhooks/deepfake-result", methods=["POST"]) def deepfake_result(): payload = request.json["payload"] call_control_id = payload["call_control_id"] result = payload["result"] score = payload.get("score") consistency = payload.get("consistency") print(f"Detection result: {result}, score: {score}, consistency: {consistency}%") if result == "fake" and score is not None and score > 0.8: # High-confidence deepfake — hang up or route to fraud queue client.calls.actions.hangup(call_control_id) print("Deepfake detected — call terminated.") elif result == "real": # Human caller — proceed normally print("Human caller verified.") # silence_timeout — caller didn't speak; handle as needed return "", 200 @app.route("/webhooks/deepfake-error", methods=["POST"]) def deepfake_error(): payload = request.json["payload"] error_message = payload["error_message"] print(f"Deepfake detection error: {error_message}") # Continue the call normally if detection fails return "", 200 if __name__ == "__main__": app.run(port=3000) ``` ```ruby Ruby require "sinatra" require "json" require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) # Answer incoming calls with deepfake detection enabled post "/webhooks/call-initiated" do payload = JSON.parse(request.body.read)["payload"] call_control_id = payload["call_control_id"] client.calls.actions.answer( call_control_id, deepfake_detection: { enabled: true, timeout: 15, rtp_timeout: 30 }, ) status 200 end # Handle deepfake detection results post "/webhooks/deepfake-result" do payload = JSON.parse(request.body.read)["payload"] call_control_id = payload["call_control_id"] result = payload["result"] score = payload["score"] consistency = payload["consistency"] puts "Detection result: #{result}, score: #{score}, consistency: #{consistency}%" if result == "fake" && score && score > 0.8 # High-confidence deepfake — hang up or route to fraud queue client.calls.actions.hangup(call_control_id) puts "Deepfake detected — call terminated." elsif result == "real" # Human caller — proceed normally puts "Human caller verified." end # silence_timeout — caller didn't speak; handle as needed status 200 end # Handle detection errors gracefully post "/webhooks/deepfake-error" do payload = JSON.parse(request.body.read)["payload"] error_message = payload["error_message"] puts "Deepfake detection error: #{error_message}" # Continue the call normally if detection fails status 200 end ``` ```go Go package main import ( "context" "encoding/json" "log" "net/http" telnyx "github.com/team-telnyx/telnyx-go/v4" "github.com/team-telnyx/telnyx-go/v4/option" ) var client = telnyx.NewClient(option.WithAPIKeyFromEnv()) type webhookRequest struct { Payload map[string]any `json:"payload"` } func callInitiated(w http.ResponseWriter, r *http.Request) { var req webhookRequest json.NewDecoder(r.Body).Decode(&req) callControlID := req.Payload["call_control_id"].(string) // Answer incoming call with deepfake detection enabled client.Calls.Actions.Answer(context.TODO(), callControlID, telnyx.CallActionAnswerParams{ DeepfakeDetection: telnyx.F(telnyx.CallActionAnswerParamsDeepfakeDetection{ Enabled: telnyx.F(true), Timeout: telnyx.F(int64(15)), RTPTimeout: telnyx.F(int64(30)), }), }) w.WriteHeader(http.StatusOK) } func deepfakeResult(w http.ResponseWriter, r *http.Request) { var req webhookRequest json.NewDecoder(r.Body).Decode(&req) p := req.Payload callControlID := p["call_control_id"].(string) result := p["result"].(string) score, _ := p["score"].(float64) consistency, _ := p["consistency"].(float64) log.Printf("Detection result: %s, score: %.2f, consistency: %.1f%%", result, score, consistency) if result == "fake" && score > 0.8 { // High-confidence deepfake — hang up or route to fraud queue client.Calls.Actions.Hangup(context.TODO(), callControlID, telnyx.CallActionHangupParams{}) log.Println("Deepfake detected — call terminated.") } else if result == "real" { // Human caller — proceed normally log.Println("Human caller verified.") } // silence_timeout — caller didn't speak; handle as needed w.WriteHeader(http.StatusOK) } func deepfakeError(w http.ResponseWriter, r *http.Request) { var req webhookRequest json.NewDecoder(r.Body).Decode(&req) errorMessage := req.Payload["error_message"].(string) log.Printf("Deepfake detection error: %s", errorMessage) // Continue the call normally if detection fails w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks/call-initiated", callInitiated) http.HandleFunc("/webhooks/deepfake-result", deepfakeResult) http.HandleFunc("/webhooks/deepfake-error", deepfakeError) log.Println("Webhook server running on port 3000") log.Fatal(http.ListenAndServe(":3000", nil)) } ``` ```java Java import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpExchange; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.calls.CallActionAnswerParams; import com.telnyx.sdk.models.calls.CallActionHangupParams; import java.io.*; import java.net.InetSocketAddress; public class DeepfakeScreening { static final TelnyxClient client = TelnyxOkHttpClient.fromEnv(); static final Gson gson = new Gson(); static JsonObject parsePayload(HttpExchange exchange) throws IOException { String body = new String(exchange.getRequestBody().readAllBytes()); return gson.fromJson(body, JsonObject.class).getAsJsonObject("payload"); } static void respond(HttpExchange exchange, int code) throws IOException { exchange.sendResponseHeaders(code, -1); exchange.close(); } public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0); // Answer incoming calls with deepfake detection enabled server.createContext("/webhooks/call-initiated", exchange -> { JsonObject payload = parsePayload(exchange); String callControlId = payload.get("call_control_id").getAsString(); client.calls().actions().answer( callControlId, CallActionAnswerParams.builder() .deepfakeDetection(CallActionAnswerParams.DeepfakeDetection.builder() .enabled(true) .timeout(15L) .rtpTimeout(30L) .build()) .build()); respond(exchange, 200); }); // Handle deepfake detection results server.createContext("/webhooks/deepfake-result", exchange -> { JsonObject payload = parsePayload(exchange); String callControlId = payload.get("call_control_id").getAsString(); String result = payload.get("result").getAsString(); double score = payload.has("score") && !payload.get("score").isJsonNull() ? payload.get("score").getAsDouble() : 0; double consistency = payload.has("consistency") && !payload.get("consistency").isJsonNull() ? payload.get("consistency").getAsDouble() : 0; System.out.printf("Detection result: %s, score: %.2f, consistency: %.1f%%%n", result, score, consistency); if ("fake".equals(result) && score > 0.8) { // High-confidence deepfake — hang up or route to fraud queue client.calls().actions().hangup( callControlId, CallActionHangupParams.builder().build()); System.out.println("Deepfake detected — call terminated."); } else if ("real".equals(result)) { // Human caller — proceed normally System.out.println("Human caller verified."); } // silence_timeout — caller didn't speak; handle as needed respond(exchange, 200); }); // Handle detection errors gracefully server.createContext("/webhooks/deepfake-error", exchange -> { JsonObject payload = parsePayload(exchange); String errorMessage = payload.get("error_message").getAsString(); System.out.println("Deepfake detection error: " + errorMessage); // Continue the call normally if detection fails respond(exchange, 200); }); server.start(); System.out.println("Webhook server running on port 3000"); } } ``` ```php PHP calls->actions->answer( callControlID: $callControlId, deepfakeDetection: [ 'enabled' => true, 'timeout' => 15, 'rtp_timeout' => 30, ], ); break; // Handle deepfake detection results case '/webhooks/deepfake-result': $result = $payload['result']; $score = $payload['score'] ?? null; $consistency = $payload['consistency'] ?? null; error_log("Detection result: {$result}, score: {$score}, consistency: {$consistency}%"); if ($result === 'fake' && $score !== null && $score > 0.8) { // High-confidence deepfake — hang up or route to fraud queue $client->calls->actions->hangup(callControlID: $callControlId); error_log('Deepfake detected — call terminated.'); } elseif ($result === 'real') { // Human caller — proceed normally error_log('Human caller verified.'); } // silence_timeout — caller didn't speak; handle as needed break; // Handle detection errors gracefully case '/webhooks/deepfake-error': $errorMessage = $payload['error_message'] ?? 'unknown'; error_log("Deepfake detection error: {$errorMessage}"); // Continue the call normally if detection fails break; } http_response_code(200); ``` ## Best practices - **Set appropriate timeouts.** The default 15-second detection timeout works well for most calls. Increase it if callers may take longer to start speaking (e.g., IVR prompts on the remote end). - **Use `score` and `consistency` together.** A high score with high consistency is a strong signal. A high score with low consistency may warrant additional verification rather than an immediate hangup. - **Handle errors gracefully.** Detection errors should not block the call. Design your application to fall through to normal call handling when detection is unavailable. ## Related resources - [Voice API Fundamentals](/docs/voice/programmable-voice/voice-api-fundamentals) - [Receiving Webhooks](/docs/voice/programmable-voice/receiving-webhooks) - [Answering Machine Detection](/docs/voice/programmable-voice/answering-machine-detection) - [Dial API Reference](/api-reference/call-commands/dial) - [Answer API Reference](/api-reference/call-commands/answer-call) --- ## Tutorials ### IVR > Source: https://developers.telnyx.com/docs/voice/programmable-voice/ivr-demo.md \| [Python](#python) | [Node](#node) | [Ruby](#ruby) | - - - ## Python ⏱ **60 minutes build time || GithHub Repo** Telnyx IVR demo built on Voice API V2 and Python with Flask and Ngrok. In this tutorial, you’ll learn how to: 1. Set up your development environment to use the Telnyx Voice API using Python and Flask. 2. Build a find me/follow me based app via IVR on the Telnyx Voice API using Python. - - - * [Prerequisites](#prerequisites-for-building-an-ivr-with-python) * [Telnyx Voice API Basics](#telnyx-call-control-basics) * [Server and Webhook Setup](#server-and-webhook-setup) * [Receiving and Interpreting Webhooks](#receiving-and-interpreting-webhooks) * [Call Commands](#call-commands) * [Client State](#client-state) * [Building the IVR](#building-the-ivr) * [Creating the IVR](#creating-the-ivr) * [Answering the Incoming Call](#answering-the-incoming-call) * [Presenting Options](#presenting-options) * [Interpreting Button Presses](#interpreting-button-presses) - - - ### Prerequisites for building an IVR with Python This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. You’ll also need to have `python` installed to continue. You can check this by running the following: ```bash $ python3 -v ``` After pasting the above content, Kindly check and remove any new line added Now in order to receive the necessary webhooks for our IVR, we will need to set up a server. For this tutorial, we will be using Flask, a micro web server framework. A quickstart guide to flask can be found on their official website. For now, we will install flask using pip. ```bash $ pip install flask ``` After pasting the above content, Kindly check and remove any new line added ### Telnyx Voice API basics For the Voice API application you’ll need to get a set of basic functions to perform Telnyx Voice API Commands. The below list of commands are just a few of the available commands available with the Telnyx Python SDK. We will be using a combination of Answer, Speak, and Gather Using Audio to create a base to support user interaction over the phone. * [Voice API Bridge Calls](/api-reference/call-commands/bridge-calls) * [Voice API Dial](/api-reference/call-commands/dial) * [Voice API Speak Text](/api-reference/call-commands/speak-text) * [Voice API Gather Using Speak](/api-reference/call-commands/gather-using-speak) * [Voice API Hangup](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup) * [Voice API Recording Start](/api-reference/call-commands/recording-start) You can get the full set of available Telnyx VoiceAPI Commands [here](/api-reference/call-commands/dial). For each Telnyx Voice API Command we will be using the Telnyx Python SDK. To execute this API we are using Python `telnyx`, so make sure you have it installed. If not you can install it with the following command: ```bash $ pip install telnyx ``` After pasting the above content, Kindly check and remove any new line added After that you’ll be able to use ‘telnyx’ as part of your app code as follows: ```python import telnyx ``` After pasting the above content, Kindly check and remove any new line added We will also import Flask in our application as follows: ```python from flask import Flask, request, Response ``` After pasting the above content, Kindly check and remove any new line added The following environmental variables need to be set Variable Description TELNYX_API_KEY Your Telnyx API Key TELNYX_PUBLIC_KEY Your Telnyx Public Key #### .env file This app uses the excellent python-dotenv package to manage environment variables. Make a copy of `.env.sample` and save as `.env` **📁 in the root directory** and update the variables to match your creds. ``` TELNYX_API_KEY= TELNYX_PUBLIC_KEY= ``` After pasting the above content, Kindly check and remove any new line added Before defining the flask application load the dotenv package and call the `load_dotenv()` function to set the environment variables. ```python from dotenv import load_dotenv load_dotenv() telnyx.api_key = os.getenv('TELNYX_API_KEY') ``` After pasting the above content, Kindly check and remove any new line added ### Server and Webhook setup Flask is a great application for setting up local servers. However, in order to make our code public to be able to receive webhooks from Telnyx, we are going to need to use a tool called ngrok. Installation instructions can be found [here](/development/development-tools/ngrok-setup/index#ngrok). Now to begin our flask application, underneath the import and setup lines detailed above, we will add the following: ```python app = Flask(__name__) @app.route('/Callbacks/Voice/Inbound', methods=['POST']) def respond(): ## Our code for handling the call control application will go here print(request.json[‘data’]) return Response(status=200) if __name__ == '__main__': app.run() ``` After pasting the above content, Kindly check and remove any new line added This is the base Flask application code specified by their documentation. This is the minimum setup required to receive webhooks and manipulate the information received in json format. To complete our setup, we must run the following to set up the Flask environment (note YOUR_FILE_NAME will be whatever you .py file is named): ```bash $ export FLASK_APP=YOUR_FILE_NAME.py ``` After pasting the above content, Kindly check and remove any new line added Now, we are ready to serve up our application to our local server. To do this, run: ```bash $ python3 app.py ``` After pasting the above content, Kindly check and remove any new line added A successful output log should look something like: ```bash * Serving Flask app "main" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) ``` After pasting the above content, Kindly check and remove any new line added Now that our Flask application is running on our local server, we can use ngrok to make this public to receive webhooks from Telnyx by running the following command wherever the ngrok executable is located (NOTE you may have to open another terminal window or push the Flask process to the background): ```bash $ ./ngrok http 5000 ``` After pasting the above content, Kindly check and remove any new line added Once this is up and running, you should see the output URL in the command logs or located on the ngrok dashboard page. This url is important because it will be where our Voice API Application will be sending webhooks to. Grab this url and head on over to the Telnyx Dashboard page. Navigate to your Call Control Application and add the URL to the section labeled "Send a webhook to the URL" as shown below. Add the ngrok url to that section and we are all set up to start our IVR! ![URL Webhook Section](/img/diagram_ivr_demo_darkmode-.png) ### Receiving and interpreting webhooks We will be configuring our respond function to handle certain incoming webhooks and execute call control commands based on what the values are. Flask catches the incoming webhooks and calls the respond() function every time a webhook is sent to the route we specified as ‘/webhook’. We can see the json value of the hook in the request.json object. Here is what a basic Telnyx Call Object looks like ```json { "data": { "event_type": "call.initiated", "id": "a2fa3fa6-4e8c-492d-a7a6-1573b62d0c56", "occurred_at": "2020-07-10T05:08:59.668179Z", "payload": { "call_control_id": "v2:rcSQADuW8cD1Ud1O0YVbFROiQ0_whGi3aHtpnbi_d34Hh6ELKvLZ3Q", "call_leg_id": "76b31010-c26b-11ea-8dd4-02420a0f6468", "call_session_id": "76b31ed4-c26b-11ea-a811-02420a0f6468", "caller_id_name": "+17578390228", "client_state": null, "connection_id": "1385617721416222081", "direction": "incoming", "from": "+14234567891", "start_time": "2020-07-10T05:08:59.668179Z", "state": "parked", "to": "+12624755500" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "http://59d6dec27771.ngrok.io/webhook" } } ``` After pasting the above content, Kindly check and remove any new line added We want to first check and see if the incoming webhook is an event. To check that, we need to look at the record_type using the following check: ```python def respond(): ## Check record_type of object data = request.json['data'] if data.get('record_type') == 'event': print(request.json[‘data’]) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added Then, we can check and see what kind of event it is. In the case of the example json above, the event is call.initiated. We can get that value using the following added code: ```python def respond(): ##Check record_type of object data = request.json['data'] if data.get('record_type') == 'event': ## Check event type event = data.get('event_type') print(event, flush=True) if event == "call_initiated": print("Incoming call", flush=True) print(request.json[‘data’]) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added As you can see, this check will print out “incoming call” whenever a call.initiated event is received by our application. We can even test it by giving the Phone Number associated with our Voice API application a call! Now we can start to implement some commands in response to this webhook. ### Call commands A full reference to the call commands in every Telnyx SDK available can be found [here](/api-reference/call-commands/dial) ### Client state `Client State`: within some of the Telnyx Voice API Commands list we presented, you probably noticed we were including the `Client State` parameter. `Client State` is the key to ensure that we can perform functions only when very specific conditions are met on our App while consuming the same Voice API Events. Because the Telnyx Voice API is stateless and async your application will be receiving several events of the same type, e.g. user just included `DTMF`. With `Client State` you enforce a unique ID to be sent back to Telnyx which be used within a particular Command flow and identifying it as being at a specific place in the call flow. This app in particular will ask the user to make a selection from various Weather stations in the US. Upon their selection, they will be transfered to the city of choice. The `client_state` is particularly useful during the transfer, as the outbound leg of the call will also emit status updates to the same endpoint as the inbound call. Setting a value to the `client_state` will allow us to check the direction of the call for the gather IVR logic. ### Building the IVR With all the basic Telnyx Voice API Commands set, we are ready to consume them and put them in the order that will create the IVR. For this tutorial we want to keep it simple with a flow that corresponds to the following IVR Logic: 1. Answer the incoming call 2. Present the options to the caller 3. Transfer the caller based on their selection ### Creating the IVR In a separate file we can create a simple class to build the Gather strings based on a simple json configuration file. The objective is to separate the IVR functionality from the spoken sentence. This will allow the IVR prompts to be updated without changing Python code. #### IVR class ```python class IVR: def __init__(self, intro, iterable, items, **kwargs): ''' Creates the IVR object by generating the initial prompt Parameters: intro (string): The introduction sentence to the IVR iterable (string): A template string to be filled in by the items items (dict): A dictionary of items with a name and phone number ''' self.intro = intro self.iterable = iterable self.items = items self.phone_number_table = {} self.valid_inputs = '' self.prompt = self.intro length = len(self.items) ## iterate over the items list and build the selection menu ## Sets the phone_number_table to lookup phone number from digit for i in range(length): itemName = self.items[i]['itemName'] phone_number = self.items[i]['phoneNumber'] digit = str(i+1) #cast to string and +1 (0-index) prompt = self.iterable % (itemName, digit) self.prompt = f'{self.prompt}, {prompt}' self.phone_number_table[digit] = phone_number self.valid_inputs = f'{self.valid_inputs}{digit}' def get_prompt(self): return self.prompt def get_valid_digits(self): return self.valid_inputs def get_phone_number_from_digit(self, digit): if (digit in self.phone_number_table): return self.phone_number_table[digit] else: return False ``` After pasting the above content, Kindly check and remove any new line added #### Instantiating the IVR class The app uses a basic JSON configuration file `ivrConfig.json` ```json { "intro": "Thank you for calling the Weather Hotline.", "iterable": "For weather in %s press %s", "items": [ { "itemName": "Chicago, Illinois", "phoneNumber": "+18158340675" }, { "itemName": "Raleigh, North Carolina", "phoneNumber": "+19193261052" } ] } ``` After pasting the above content, Kindly check and remove any new line added To Instantiate the IVR class we'll need to: 1. Read the file 2. Covert the JSON to a dict 3. Create the class ```python import json def open_IVR_config_json(file_name): with open(file_name) as json_file: data = json.load(json_file) return data ivr_config = open_IVR_config_json('ivrConfig.json') my_ivr = IVR(intro = ivr_config['intro'], iterable = ivr_config['iterable'], items = ivr_config['items']) ``` After pasting the above content, Kindly check and remove any new line added We'll use the `my_ivr` as a global variable for the Flask route to generate prompt strings and check the user pressed digits. ```python import telnyx import os import base64 import json from flask import Flask, request, Response from dotenv import load_dotenv from ivr import IVR load_dotenv() telnyx.api_key = os.getenv('TELNYX_API_KEY') def open_IVR_config_json(file_name): with open(file_name) as json_file: data = json.load(json_file) return data ivr_config = open_IVR_config_json('ivrConfig.json') my_ivr = IVR(intro = ivr_config['intro'], iterable = ivr_config['iterable'], items = ivr_config['items']) app = Flask(__name__) @app.route('/Callbacks/Voice/Inbound', methods=['POST']) def respond(): global my_ivr data = request.json.get('data') print(data) if data.get('record_type') == 'event': # Check event type event = data.get('event_type') print(event, flush=True) call_control_id = data.get('payload').get('call_control_id') my_call = telnyx.Call() my_call.call_control_id = call_control_id if event == 'call.initiated': print("Incoming call", flush=True) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added ### Answering the Incoming Call Now, we can add a simple Call command to answer the incoming call. Underneath where we check if the event is `call_initiated`. To keep track of which call is which; we'll set the direction to the `client_state` using pythons native base64 encoding. 👀 At the **top** ⬆️ of the `app.py` file add `import base64` ```python if event == 'call.initiated': direction = data.get('payload').get('direction') if (direction == 'incoming'): encoded_client_state = base64.b64encode(direction.encode('ascii')) client_state_str = str(encoded_client_state, 'utf-8') res = my_call.answer(client_state=client_state_str) print(res, flush=True) ``` After pasting the above content, Kindly check and remove any new line added This code snippet does a few things: 1. Base64encodes the direction value 2. Sets as client_state 3. actually answers the call. ### Presenting options Now that we have answered the call, we can use the `Gather Using Speak` command to present some options to the user. To do this, we will check the event **and** check to see that `client_state` exists. The outbound transferred call leg will also emit the `call.answered` event; however, the `client_state` value will be null. Otherwise, the called party would also be presented with the gather prompt. ```python elif event == 'call.answered': client_state = data.get('payload').get('client_state') if (client_state): speak_str = my_ivr.get_prompt() res = my_call.gather_using_speak( payload=speak_str, valid_digits=my_ivr.get_valid_digits(), language = 'en-US', voice = 'male') print(res, flush=True); ``` After pasting the above content, Kindly check and remove any new line added Using the `my_ivr` object we created earlier, we can send `Gather Using Speak` audio to the number. This code present the caller with the generated prompt `my_ivr.get_prompt()` ### Interpreting button presses Our next check will be to see what digit is pressed when the gather has completed & sends the `call.gather.ended` event. We'll extract the digits from the payload and use our instantiated IVR class to lookup the transfer number. Finally, we'll send the transfer command to Telnyx to transfer the user to their destination. ```python # When gather is ended, collect the digit pressed and speak them elif event == 'call.gather.ended': digits_pressed = data.get('payload').get('digits') phone_number_lookup = my_ivr.get_phone_number_from_digit(digits_pressed) if (phone_number_lookup): to = phone_number_lookup res = my_call.transfer(to=to) print(res, flush=True) ``` After pasting the above content, Kindly check and remove any new line added ### Conclusion Take a look at the GithHub Repo for a commented version of this code to use as a base for your IVR application! ## Node ⏱ **60 minutes build time || Github Repo** Telnyx Find Me/Follow Me IVR demo built on the Telnyx Voice API V2 and node.js. In this tutorial, you’ll learn how to: 1. Set up your development environment to use Telnyx Voice API using Node. 2. Build a find me/follow me based app via IVR on Telnyx Voice API using Node. - - - * [Prerequisites](#prerequisites-for-building-an-ivr-with-node) * [Telnyx Voice API Basics](#the-basics-of-telnyx-call-control) * [Understanding the Use of The SDK](#understanding-the-use-of-the-sdk) * [Telnyx Voice API Commands](#telnyx-call-control-commands) * [Building Find Me Follow Me IVR](#building-find-me-follow-me-ivr) * [Lightning-Up the Application](#lightning-up-the-application) - - - ### Prerequisites for building an IVR with node This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using Voice API. You’ll also need to have `node` installed to continue. You can check this by running the following: ```bash $ node -v ``` After pasting the above content, Kindly check and remove any new line added If Node isn’t installed, follow the official installation instructions for your operating system to install it. You’ll need to have the following Node dependencies installed for the Voice API: ```javascript import express from 'express'; // or any similar library import Telnyx from 'telnyx'; ``` After pasting the above content, Kindly check and remove any new line added ### The Basics of Telnyx Voice API For the Voice API application you’ll need to get a set of basic functions to perform Telnyx Voice API Commands. This tutorial will be using the following subset of basic Telnyx Voice API Commands: * [Voice API Bridge Calls](/api-reference/call-commands/bridge-calls) * [Voice API Dial](/api-reference/call-commands/dial) * [Voice API Speak Text](/api-reference/call-commands/speak-text) * [Voice API Gather Using Speak](/api-reference/call-commands/gather-using-speak) * [Voice API Hangup](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup) * [Voice API Recording Start](/api-reference/call-commands/recording-start) You can get the full set of available Telnyx Voice API Commands [here](/api-reference/call-commands/dial). For each Telnyx Voice API Command we will be using the Telnyx Node SDK. To execute this API we are using Node `telnyx`, so make sure you have it installed. If not you can install it with the following command: ```bash $ npm install telnyx --save ``` After pasting the above content, Kindly check and remove any new line added After that you’ll be able to use ‘telnyx’ as part of your app code as follows: ```javascript import Telnyx from 'telnyx'; const telnyx = new Telnyx("YOUR_API_KEY"); ``` After pasting the above content, Kindly check and remove any new line added To make use of the Telnyx Voice API Command API you’ll need to set a Telnyx API Key and Secret. To check that go to Mission Control Portal and under the `Auth` tab you select `Auth V2`. Once you have them, you can include it as ‘const’ variable in your code: ```javascript import telnyxAuth from "./telnyx-config"; const telnyx = new Telnyx(telnyxAuth.apiKey); ``` After pasting the above content, Kindly check and remove any new line added We have a number of secure credentials to work with we created an additional file `telnyx-config` to store this information. Here we will store our API Key as well as our connection ID, the DID associated with that connection and the PSTN DID we will send calls to. ```javascript export const telnyx_config = { api: "YOURAPIV2KEYgoeshere", connection_id: "1110011011", telnyx_did: "+18888675309", c_fwd_number: "+13128675309" }; ``` After pasting the above content, Kindly check and remove any new line added Once all dependencies are set, we will use the SDK for each Telnyx Voice API Command. All Commands will follow the similar syntax: ```javascript const { data: call } = await telnyx.calls.create({ connection_id: g_connection_id, to: g_forwarding_did, from: req.body.data.payload.from, client_state: `base64encodedstring`}); ``` After pasting the above content, Kindly check and remove any new line added #### Understanding the use of the SDK There are several aspects of the SDK that deserve some attention: * `Input Parameters`: to execute every Telnyx Voice API Command you’ll need to feed your function with the following: * the `Call Control ID` * the input parameters, specific to the body of the Command you’re executing. ```javascript const gather = new telnyx.Call({ call_control_id: l_call_control_id,}); gather.gather_using_speak({ payload: "Call Forwarded press 1 to accept or 2 to reject", voice: g_ivr_voice, language: g_ivr_language, valid_digits: "123", client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64")}); ``` After pasting the above content, Kindly check and remove any new line added All Telnyx Voice API Commands will be expecting the `Call Control ID` except `Dial`. There you’ll get a new one for the leg generated as response. In this example you can see that `Call Control ID` is input to the Telnyx Call Object. The command to utilize is then specifed when the new Call Object is called with the input paramters pertaining to that command. #### Telnyx Voice API commands This is how every Telnyx Voice API Command used in this application looks: #### Voice API bridge ```javascript const bridge_call = new telnyx.Call({ call_control_id: l_call_control_id,}); bridge_call.bridge({ call_control_id: l_client_state_o.bridgeId, }); ``` After pasting the above content, Kindly check and remove any new line added #### Voice API dial ```javascript const { data: call } = await telnyx.calls.create({ connection_id: g_connection_id, to: g_forwarding_did, from: req.body.data.payload.from, client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64"), timeout_secs: "30", }); ``` After pasting the above content, Kindly check and remove any new line added #### Voice API gather using speak ```javascript const gather = new telnyx.Call({ call_control_id: l_call_control_id,}); gather.gather_using_speak({ payload: "Call Forwarded press 1 to accept or 2 to reject", voice: g_ivr_voice, language: g_ivr_language, valid_digits: "123", client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64"), }); ``` After pasting the above content, Kindly check and remove any new line added #### Voice API speak ```javascript const speak = new telnyx.Call({ call_control_id: l_call_control_id}); speak.speak({ payload: "Please Leave a Message After the Tone", voice: g_ivr_voice, language: g_ivr_language, client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64"), }); ``` After pasting the above content, Kindly check and remove any new line added #### Voice API hangup ```javascript const hangup_call = new telnyx.Call({ call_control_id: l_call_control_id}); hangup_call.hangup(); ``` After pasting the above content, Kindly check and remove any new line added #### Voice API recording start ```javascript const record_call = new telnyx.Call({ call_control_id: l_call_control_id}); record_call.record_start({ format: "mp3", channels: "single", play_beep: true, client_state: Buffer.from(JSON.stringify(l_client_state)).toString( "base64" ),}); ``` After pasting the above content, Kindly check and remove any new line added #### SMS send notification ```javascript telnyx.messages.create({ from: g_call_control_did, // Your Telnyx number to: g_forwarding_did, text: `You have a new Voicemail${req.body.data.payload.recording_urls.mp3}`, }) .then(function(response) { const message = response.data; // asynchronously handled }); ``` After pasting the above content, Kindly check and remove any new line added ### The client state parameter `Client State`: within some of the Telnyx Call Control Commands list we presented, you probably noticed we were including the `Client State` parameter. `Client State` is the key to ensure that we can perform functions only when very specific conditions are met on our App while consuming the same Call Control Events. Because the Telnyx Voice API is stateless and async your application will be receiving several events of the same type, e.g. user just included `DTMF`. With `Client State` you enforce a unique ID to be sent back to Telnyx which be used within a particular Command flow and identifying it as being at a specific place in the call flow. This app in particular will bridge two seperate calls together in the event the user chooses to accept the call. Thus the call_control_id of the pending bridge call must be mapped, and not be risked to being stored in a variable which could be re-assigned while we are waiting for gather response - should a new call be intiated. #### Build client state object and encode to base64 ```javascript // Build Client State Object let l_client_state = { clientState: "stage-bridge", bridgeId: l_call_control_id, }; // Object to String and Encode to Base64 Buffer.from( JSON.stringify(l_client_state) ).toString("base64") // When we receive the hook - If client_state exists decode from base64 if (l_client_state_64 != null || "") const l_client_state_o = JSON.parse( Buffer.from(l_client_state_64, "base64").toString("ascii") ); ``` After pasting the above content, Kindly check and remove any new line added ### Building find me follow me IVR With all the basic Telnyx Voice API Commands set, we are ready to consume them and put them in the order that will create the IVR. For this tutorial we want to keep it simple with a flow that corresponds to the following IVR Logic: 1. Allow the incoming call to be parked. 2. Execute dial function to the user's PSTN number. 3. Present an IVR allowing them to Accept or Reject the call and execute a 20 second timeout to hangup for no answer. 4. When the user answers, they will be met with an IVR Greeting: * Press 1 to Accept the Call - The Parked Call and this Dialed call will now be Bridged. The Timeout to Hangup the Dial call to user will be cleared. * Press 2 to Reject the call - The Dialed Call will hang up. The Parked call will enter the Voicemail Functionality via Speak and Recording Start * At any time during the caller, the user can press *9 to initiate on demand call recording. 5. An SMS notification will be sent to the user to notify them of a call recording or voicemail message. (Optionally) - the nodemailer function will send an email to the user with a link to download and listen to the recording. ![IVR Demo Diagram](https://images.ctfassets.net/4b49ta6b3nwj/5B6v9Bygi4iVGH8N42C1Hw/a663b0e9619b95e3b4d1df4c8749e611/Diagram_IVR_Demo_DarkMode.png) To exemplify this process we created a simple API call that will be exposed as the webhook in Mission Portal. For that we would be using `express`: ```bash $ npm install request --save ``` After pasting the above content, Kindly check and remove any new line added With `express` we can create an API wrapper that uses `HTTP GET` to call our Request Token method: ```javascript rest.post(`/${g_appName}/followme`, async (req, res) => { // APP CODE GOES HERE }) ``` After pasting the above content, Kindly check and remove any new line added This would expose a webhook like the following: ``` http://MY_DOMAIN_URL/telnyx-findme/followme ``` You probably noticed that `g_appName` in the previous point. That is part of a set of global variables we are defining with a certain set of info we know we are going to use in this app: TTS parameters, like voice and language to be used and IVR redirecting contact points. You can set these at the beginning of your code: ```javascript // Application: // Application: const g_appName = "telnyx-findme"; // TTS Options const g_ivr_voice = "female"; const g_ivr_language = "en-GB"; ``` After pasting the above content, Kindly check and remove any new line added With that set, we can fill in that space that we named as `APP CODE GOES HERE`. So as you expose the URL created as Webhook in Mission Control associated with your number, you’ll start receiving all call events for that call. So the first thing to be done is to identify the kind of event you just received and extract the `Call Control Id` and `Client State` (if defined previously): ```javascript if (req && req.body && req.body.event_type){ if (req && req.body && req.body.data.event_type) { const l_hook_event_type = req.body.data.event_type; const l_call_control_id = req.body.data.payload.call_control_id; const l_client_state_64 = req.body.data.payload.client_state; } else{res.end('0');} ``` After pasting the above content, Kindly check and remove any new line added Once you identify the `Event Type` and `client_state` received, it’s just a matter of having your application reacting to that. Is the way you react to that Event that helps you creating the IVR logic. What you would be doing is to execute Telnyx Call Control Command as a reaction to those Events. #### `Webhook call initiated >> Command answer call` If our `event_type` is call.initiated and the direction is incoming we are going to execute the command to Dial the User. After the Dial is executed and we get a new webhook for the dialed call which the direction will be "outgoing," we will specify our `timeout_secs` parameter to 30 seconds so that the user's mobile voicemail doesn't pick up and we leave an empty message there ```javascript if (l_hook_event_type == "call.initiated") { // Inbound Call if (req.body.data.payload.direction == "incoming") { // Format the update to client-state so we can execute call flow and the call control id of the call we may eventually bridge follows in client_state let l_client_state = { clientState: "stage-bridge", bridgeId: l_call_control_id, }; // Dial to our FindMe/FollowMe Destination, forwarding the original CallerID so we can better determine disposition of choice const { data: call } = await telnyx.calls.create({ connection_id: g_connection_id, to: g_forwarding_did, from: req.body.data.payload.from, client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64"), timeout_secs: "30", }); console.log( `[%s] LOG - EXEC DIAL - [%s] ${get_timestamp()} | ${ req.body.data.payload.result }` ); res.end(); ``` After pasting the above content, Kindly check and remove any new line added #### `Webhook dial answered >> Command gather using speak` Once your app is notified by Telnyx that the call was established you want to initiate your IVR. You do that using the Telnyx Voice API Command `Gather Using Speak`, with the IVR message. As part of the `Gather Using Speak` Command we indicate that valid digits for the `DTMF` collection are 1 and 2, and that only 1 digit input would be valid. Since we only want to execute this when the call is answered by the user via the dial, we set `client_state` to "stage-bridge" on the Dial seen above. ```javascript else if (l_hook_event_type == "call.answered") { if (l_client_state_o.clientState == "stage-bridge") { let l_client_state = { clientState: "stage-dial", bridgeId: l_client_state_o.bridgeId, }; // Gather Using Speak - Present Menu to Forwading destination, 1 to Accept and Bride Call, 2 to Reject and Send to System Voicemail const gather = new telnyx.Call({ call_control_id: l_call_control_id, }); gather.gather_using_speak({ payload: "Call Forwarded press 1 to accept or 2 to reject", voice: g_ivr_voice, language: g_ivr_language, valid_digits: "123", client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64"), }); console.log(`[%s] LOG - EXEC GATHER - [%s] ${get_timestamp()}`); res.end(); } ``` After pasting the above content, Kindly check and remove any new line added *Important Note: For consistency, Telnyx Voice API requires every single Webhook to be replied by the Webhook end-point, otherwise will keep trying. For that reason we have to be ready to consume every Webhook we expect to receive and reply with `200 OK`.* #### `Webhook call bridged >> Do nothing` Your app will be informed that the call was bridged should the user choose to accept the call. For the APP we are doing nothing with that info, but we will need to reply to that command. ```javascript else if (l_hook_event_type == call_bridged){ res.end(); } ``` After pasting the above content, Kindly check and remove any new line added #### `Webhook listen for DTMF to execute call recording on demand` We need to be listening for the specified digit in order to execute the recording on demand feature, specifically \*\**. Now this example is very rudimentary and is just for proof of concept. In production, the dtmf should only be received from the user's call leg. Additionally here, we will empty the array once the condition is met and we execute the `Recording Start` Command. We are also re-using this to record are voicemail message. ```javascript else if ( req.body.data.payload.digit === "*" || l_hook_event_type == "call.speak.ended" ) { let l_client_state = { clientState: "stage-voicemail-greeting", bridgeId: null, }; const record_call = new telnyx.Call({ call_control_id: l_call_control_id, }); record_call.record_start({ format: "mp3", channels: "single", play_beep: true, client_state: Buffer.from(JSON.stringify(l_client_state)).toString( "base64" ), }); console.log( `[%s] LOG - EXEC RECORD INITIATE - [%s] ${get_timestamp()}` ); res.end(); } ``` After pasting the above content, Kindly check and remove any new line added *Important Note: With DTMF, you will recieve both dtmf in the payload of webhooks for both `call.gather.ended` and `call.dtmf.received`. The main difference is that in the gather webhooks dtmf will be sent as value to key "digits" and in dtmf.received the key will be "digit."* #### `Webhook gather ended >> Find me IVR logic` It’s when you receive the Webhook informing your application that Voice API `Gather Ended` (DTMF input) that the IVR magic happens: We're doing a number of things here. 1. If the user presses 1, we are first going to clear the timeout for this Dialed call so it does not hangup automatically. Second, we are going to issue "bridge" to connect the caller and the user. 2. If the user presses 2, we are going to do execute two commands. We will speak the voicemail greeting to the caller, and issue hangup to the users mobile. In order to bridge the calls, we need both the call_control_id for this Dialed Call and the call_control_id PSTN Caller. This is the call_control_bridge function you see we are passing. * `l_call_control_id` The call control id of the latest webhook we just recieved the DTMF on and has a `client_state` of "stage-dial" * `l_bridge_id` The PSTN caller's call control id, we set that variable to our client state object in `l_client_state.bridgeId` earlier when we first received the webhook on the incoming call. We've been receiving webhooks for both the original PSTN caller and for the new call we placed via Dial to the user. Both have their own unique call_control_ids, which we will use to bridge both calls together. Here you will witness the importance of `client_state` as we're only executing the bridge on the dial webhook that we set `client_state` of "stage-dial". #### `Webhook gather ended >> Process DTMF for IVR` ```javascript else if (l_hook_event_type == "call.gather.ended") { // Receive DTMF Number const l_dtmf_number = req.body.data.payload.digits; console.log( `[%s] DEBUG - RECEIVED DTMF [%s]${get_timestamp()} | ${l_dtmf_number}` ); res.end(); // Check Users Selection for forwarded call if (!l_client_state_64) { res.end(); // Do nothing... will have state } else { // Selected Answer Call >> Bridge Calls if (l_client_state_o.clientState == "stage-dial" && l_dtmf_number) { // Bridge Call if (l_dtmf_number == "1") { const bridge_call = new telnyx.Call({ call_control_id: l_call_control_id, }); // Bridge this call to the initial call control id which triggered our call flow which we stored in client state on the initial Dial bridge_call.bridge({ call_control_id: l_client_state_o.bridgeId, }); res.end(); console.log( `[%s] LOG - EXEC BRIDGE CALLS - [%s] ${get_timestamp()}` ); // Call rejected >> Answer Bridge Call, You must answer the parked call before you can issue speak or play audio } else if (l_dtmf_number == "2") { // Set Call State so we can initiate the voicemail call flow let l_client_state = { clientState: "stage-voicemail-greeting", bridgeId: null, }; const answer_bridge_call = new telnyx.Call({ call_control_id: l_client_state_o.bridgeId, }); answer_bridge_call.answer({ client_state: Buffer.from( JSON.stringify(l_client_state) ).toString("base64"), }); // Hangup This call now that user has responded to reject const hangup_call = new telnyx.Call({ call_control_id: l_call_control_id, }); hangup_call.hangup(); console.log( `[%s] LOG - EXEC HANGUP FINDME AND SEND TO VM - [%s] ${get_timestamp()}` ); } res.end(); } } res.end(); // Webhook Speak Ended or * received >> Record VoiceMail / Call } ``` After pasting the above content, Kindly check and remove any new line added #### `Webhook call recording saved >> Send text message of recording` We are receiving a webhook of `call.recording.saved` after BOTH a voicemail has been recorded and if a record call on demand has been executed. Now in this web hook we will recieve a link to an mp3 recording of either the voicemail or recorded call. We are going to send an sms notification to the User via `sms_send_notification`. Optionally, we are using the nodemailer sdk to send an email to the user with the link so they can listen to the message or call. ```javascript else if (l_hook_event_type == "call.recording.saved") { //Send Text Message Alert for call recording - Ber sure to enable Link shortener in Telnyx Messaging Profile telnyx.messages .create({ from: g_call_control_did, // Your Telnyx number to: g_forwarding_did, text: `You have a new Recording ${req.body.data.payload.recording_urls.mp3}`, }) .then(function(response) { const message = response.data; // asynchronously handled }); console.log(`[%s] LOG - EXEC SEND SMS - [%s] ${get_timestamp()}`); res.end(); } ``` After pasting the above content, Kindly check and remove any new line added ### Lightning-up the application Finally the last piece of the puzzle is having your application listening for Telnyx Webhooks: ```javascript const PORT = 8081; rest.listen(PORT, () => { console.log( `SERVER ${get_timestamp()} - app listening at http://localhost:${PORT}/${g_appName}` ); }); }) ``` After pasting the above content, Kindly check and remove any new line added And start the application by executing the following command: ```javascript $ npm run dev ``` After pasting the above content, Kindly check and remove any new line added ## Ruby ⏱ **30 minutes build time** ### Introduction to the call control framework The [Voice API framework](/api-reference/call-commands/dial), previously called Call Control, is a set of APIs that allow complete control of a call flow from the moment a call begins to the moment it is completed. In between, you will receive a number of [webhooks](/docs/voice/programmable-voice/receiving-webhooks) for each step of the call, allowing you to act on these events and send commands using the Telnyx Library. A subset of the operations available in the Call Control API is the [Call Control Conference](/api-reference/conference-commands/conference-recording-start) API. This allows the user (you) to create and manage a conference programmatically upon receiving an incoming call, or when initiating an outgoing call. The Telnyx Ruby Library is a convenient wrapper around the Telnyx REST API. It allows you to access and control call flows using an intuitive object-oriented library. This tutorial will walk you through creating a simple Sinatra server that allows you to create an IVR demo application. ### Setup your development environment Before beginning, please ensure that you have the Telnyx, Dotenv, and Sinatra gems installed. ```bash gem install telnyx sinatra dotenv ``` After pasting the above content, Kindly check and remove any new line added Alternatively, create a Gemfile for your project ```ruby source 'https://rubygems.org' gem 'sinatra' gem 'telnyx' gem 'dotenv' ``` After pasting the above content, Kindly check and remove any new line added #### Setting environment variables The following environmental variables need to be set Variable Description TELNYX_API_KEY Your Telnyx API Key TELNYX_PUBLIC_KEY Your Telnyx Public Key TELNYX_APP_PORT Defaults to 8000 The port the app will be served #### .env file This app uses the excellent dotenv package to manage environment variables. Make a copy of the file below, add your credentials, and save as `.env` in the root directory. ``` TELNYX_API_KEY= TELNYX_PUBLIC_KEY= TENYX_APP_PORT=8000 ``` After pasting the above content, Kindly check and remove any new line added #### Portal setup This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using Voice API. The Voice API Application needs to be setup to send API V2 webhooks: * Make sure the *Webhook API Version* is **API v2**. * Fill in the *Webhook URL* with the address the server will be running on. Alternatively, you can use a service like Ngrok to temporarily forward a local port to the internet to a random address and use that. We'll talk about this in more detail later. Finally, you need to create an API Key - make sure you save the key somewhere safe. Now create a file such as `ivr_demo_server.rb`, then write the following to setup the Telnyx library. ```ruby # frozen_string_literal: true require 'sinatra' require 'telnyx' require 'dotenv/load' # Setup telnyx api key. Telnyx.api_key = ENV.fetch('TELNYX_API_KEY') ``` After pasting the above content, Kindly check and remove any new line added ### Receiving webhooks & answering a call Now that you have setup your auth token and `call_control_id`, you can begin to use the API Library to answer a call and receive input from DTMF. First, you will need to setup a Sinatra endpoint to receive webhooks for call and DTMF events. There are a number of webhooks that you should anticipate receiving during the lifecycle of each call. This will allow you to take action in response to any number of events triggered during a call. In this example, you will use the `call.initiated` and `call.answered` events to answer the incoming call and then present IVR options to the user. You'll use the `call.gather.ended` event to parse the digits pressed during the IVR. ```ruby # ... set :port, ENV.fetch('TELNYX_APP_PORT') post '/webhook' do # Parse the request body. request.body.rewind body = request.body.read # Save the body for verification later data = JSON.parse(body)['data'] # Handle events if data['record_type'] == 'event' call = Telnyx::Call.new id: data['payload']['call_control_id'], call_leg_id: data['payload']['call_leg_id'] case data['event_type'] when 'call.initiated' # Answer the call, this will cause the api to send another webhook event # of the type call.answered, which we will handle below. call.answer puts('Answered Call') when 'call.answered' # Start to gather information, using the prompt "Press a digit" call.gather_using_speak(voice: 'female', language: 'en-US', payload: 'Press some digits! The only valid options are 1 2 3', valid_digits: '123', invalid_payload: 'Invalid Entry Please try again') puts('Gather sent') when 'call.gather.ended' # Only care about the digits captured during the gather request if data['payload']['status'] != 'call_hangup' # Ensure that the reason for ending was NOT a hangup (can't speak on an ended call) call.speak(voice: 'female', language: 'en-US', payload: "You pressed: #{data['payload']['digits']}, You can now hangup") puts('DTMF spoke') end end end end ``` After pasting the above content, Kindly check and remove any new line added Pat youself on the back - that's a lot of code to go through! Now let's break it down even further and explain what it does. First, create an array for keeping track of the ongoing calls so that we can differentiate. Then, tell Sinatra to listen on the port defined in the `.env` file and create an endpoint at `/webhook`, which can be anything you choose as the API doesn't care; here we just call it webhook. ```ruby set :port, ENV.fetch('TELNYX_APP_PORT') post "/webhook" do # ... end ``` After pasting the above content, Kindly check and remove any new line added Next, parse the data from the API server, check to see if it is a webhook event, and act on it if it is. Then, you will define what actions to take on different types of events. The webhook endpoint is only tuned to accept call events. You can create a `call` object from the `call_control_id` nested in the `webhook.data.payload` JSON. This will allow you to send commands to the active call. ```ruby post '/webhook' do # Parse the request body. request.body.rewind body = request.body.read # Save the body for verification later data = JSON.parse(body)['data'] # Handle events if data['record_type'] == 'event' call = Telnyx::Call.new id: data['payload']['call_control_id'], call_leg_id: data['payload']['call_leg_id'] case data['event_type'] end end end ``` After pasting the above content, Kindly check and remove any new line added Here is where you will respond to a new call being initiated, which can be from either an inbound or outbound call. Create a new `Telnyx::Call` object and store it in the active call list, then call `call.answer` to answer it if it's an inbound call. ```ruby when 'call.initiated' # Answer the call, this will cause the api to send another webhook event # of the type call.answered, which we will handle below. call.answer puts('Answered Call') ``` After pasting the above content, Kindly check and remove any new line added On the `call.answered` event, we can call the `gather_using_speak` command to speak audio and gather DTMF information from the user input. Take note that the `valid_digits` restricts the input to the caller to only the digits specified. The `invalid_payload` will be played back to the caller before the `payload` is repeated back if any invalid digits are pressed when the gather completes. ```ruby when 'call.answered' # Start to gather information, using the prompt "Press a digit" call.gather_using_speak(voice: 'female', language: 'en-US', payload: 'Press some digits! The only valid options are 1 2 3', valid_digits: '123', invalid_payload: 'Invalid Entry Please try again') puts('Gather sent') ``` After pasting the above content, Kindly check and remove any new line added Now, once we have our setup complete, when the gather is complete due to one of the statuses: ( `valid`, `invalid`, `call_hangup`, `cancelled`, `cancelled_amd`), the `call.gather.ended` event is sent to the `webhook` endpoint. From there, we can extract the `digits` field from the `payload` and play it back to the user using `speak`. Take note that the `call_hangup` status indicates the caller hungup before the gather could complete. For that case, we're done as `speak` does not work on an ended call. ```ruby when 'call.gather.ended' # Only care about the digits captured during the gather request if data['payload']['status'] != 'call_hangup' # Ensure that the reason for ending was NOT a hangup (can't speak on an ended call) call.speak(voice: 'female', language: 'en-US', payload: "You pressed: #{data['payload']['digits']}, You can now hangup") puts('DTMF spoke') end end ``` After pasting the above content, Kindly check and remove any new line added ### Authentication for your calls Now you have a working conference application! How secure is it though? Could a third party simply craft fake webhooks to manipulate the call flow logic of your application? Telnyx has you covered with a powerful signature verification system! Make the following changes: ```ruby # ... post '/webhook' do # Parse the request body. request.body.rewind body = request.body.read # Save the body for verification later data = JSON.parse(body)['data'] begin Telnyx::Webhook::Signature.verify(body, request.env['HTTP_TELNYX_SIGNATURE_ED25519'], request.env['HTTP_TELNYX_TIMESTAMP']) rescue Exception => e puts e halt 400, 'Webhook signature not valid' end # ... ``` After pasting the above content, Kindly check and remove any new line added Your public key is read from the Environment variables defined in your `.env` file. Look up your public key from the Telnyx Portal here. `Telnyx::Webhook::Signature.verify` will do the work of verifying the authenticity of the message, and raise `SignatureVerificationError` if the signature does not match the payload. ### Final `ivr_demo_server.rb` All together, your `ivr_demo_server.rb` file should resemble something like: ```ruby # frozen_string_literal: true require 'sinatra' require 'telnyx' require 'dotenv/load' # Setup telnyx api key. Telnyx.api_key = ENV.fetch('TELNYX_API_KEY') set :port, ENV.fetch('TELNYX_APP_PORT') post '/webhook' do # Parse the request body. request.body.rewind body = request.body.read # Save the body for verification later data = JSON.parse(body)['data'] begin Telnyx::Webhook::Signature.verify(body, request.env['HTTP_TELNYX_SIGNATURE_ED25519'], request.env['HTTP_TELNYX_TIMESTAMP']) rescue Exception => e puts e halt 400, 'Webhook signature not valid' end # Handle events if data['record_type'] == 'event' call = Telnyx::Call.new id: data['payload']['call_control_id'], call_leg_id: data['payload']['call_leg_id'] case data['event_type'] when 'call.initiated' # Answer the call, this will cause the api to send another webhook event # of the type call.answered, which we will handle below. call.answer puts('Answered Call') when 'call.answered' # Start to gather information, using the prompt "Press a digit" call.gather_using_speak(voice: 'female', language: 'en-US', payload: 'Press some digits! The only valid options are 1 2 3', valid_digits: '123', invalid_payload: 'Invalid Entry Please try again') puts('Gather sent') when 'call.gather.ended' # Only care about the digits captured during the gather request if data['payload']['status'] != 'call_hangup' # Ensure that the reason for ending was NOT a hangup (can't speak on an ended call) call.speak(voice: 'female', language: 'en-US', payload: "You pressed: #{data['payload']['digits']}, You can now hangup") puts('DTMF spoke') end end end end ``` After pasting the above content, Kindly check and remove any new line added ### Voice API Usage If you used a Gemfile, start the conference server with `bundle exec ruby ivr_demo_server.rb`, if you are using globally installed gems use `ruby ivr_demo_server.rb`. When you are able to run the server locally, the final step involves making your application accessible from the internet. So far, we've set up a local web server. This is typically not accessible from the public internet, making testing inbound requests to web applications difficult. The best workaround is a tunneling service. They come with client software that runs on your computer and opens an outgoing permanent connection to a publicly available server in a data center. Then, they assign a public URL (typically on a random or custom subdomain) on that server to your account. The public server acts as a proxy that accepts incoming connections to your URL, forwards (tunnels) them through the already established connection and sends them to the local web server as if they originated from the same machine. The most popular tunneling tool is `ngrok`. Check out the [ngrok setup](/development/development-tools/ngrok-setup/index#ngrok) walkthrough to set it up on your computer and start receiving webhooks from inbound messages to your newly created application. Once you've set up `ngrok` or another tunneling service you can add the public proxy URL to your Connection in the MIssion Control Portal. To do this, click the edit symbol \[✎] next to your Connection. In the "Webhook URL" field, paste the forwarding address from ngrok into the Webhook URL field. Add `/webhook` to the end of the URL to direct the request to the webhook endpoint in your Sinatra server. #### Callback URLs for Telnyx applications Callback Type URL Inbound Calls Callback `{ngrok-url}/webhook` For now you'll leave “Failover URL” blank, but if you'd like to have Telnyx resend the webhook in the case where sending to the Webhook URL fails, you can specify an alternate address in this field. ### Complete running Voice API IVR application You have now created a simple IVR application! Using other Call Commands, you can perform actions based on user input collected during a gather. For more information on what call commands you can use, check out the [Call Command Documentation](/api-reference/call-commands/dial "Call Command Documentation")s --- ### Call Center > Source: https://developers.telnyx.com/docs/voice/programmable-voice/call-center.md __⏱ 30 minutes build time.__ __🧰 Clone the sample application from our GitHub repo.__ __🎬 Check out our video walkthrough of this tutorial.__ ## Introduction In this tutorial, you'll learn how to build a call center application using the __Telnyx Voice API__, __TeXML__, and the __Python AIOHTTP library__, in three main parts: 1. Set up and configure your Mission Control Portal to link a call center number to your call center agents' TeXML-enabled SIP connections. 2. Set up and run the sample call center application in your preferred environment. 3. Optional: Configure your call center app with customized audio playback, hangup messages, and voicemail storage. The app accepts calls to a Telnyx number and forwards them to one or more SIP Connections associated with your call center agents (desk phones or softphones) using SIP URI calling. The agents must be registered with Telnyx SIP proxies to receive these forwarded calls. The app's basic call flow is as follows: > 1. A user calls the main phone number associated with the call center, and the call is answered with a text-to-speech greeting. > 2. The call is forwarded to multiple agents simultaneously, with call recording enabled. > 3. If one of the agents answers the call, the other agents stop ringing. > 4. If no agent answers the call, a second text-to-speech message is played, and the agents are dialed for a second time. > 5. If no agent answers on the second dialing attempt, the user may leave a voicemail recording. > 6. If the call is answered and subsequently ended, a text-to-speech message will thank the user for calling. ```mermaid sequenceDiagram participant User participant Telnyx as Telnyx TeXML participant Agents as Call Center Agents User->>Telnyx: Calls main number Telnyx->>User: TTS greeting Telnyx->>Agents: Ring all agents (attempt 1) alt Agent answers Agents->>Telnyx: Answer + recording starts Telnyx-->>Agents: Stop ringing others Note over User,Agents: Call connected Agents->>Telnyx: Hang up Telnyx->>User: TTS thank you else No answer Telnyx->>User: TTS hold message Telnyx->>Agents: Ring all agents (attempt 2) alt Agent answers Agents->>Telnyx: Answer else No answer again Telnyx->>User: Voicemail prompt User->>Telnyx: Leave voicemail end end ``` All of these steps in the call flow can be modified, configured, or built upon by editing TeXML files in the `/call_center/infrastructure/TeXML` directory of the repo. __Jump to section:__ - [Creating a Telnyx Mission Control Portal account](#step-1-create-a-telnyx-mission-control-portal-account) - [Creating a Telnyx API key](#step-2-create-a-telnyx-api-key) - [Installing ngrok](#step-3-install-and-run-ngrok) - [Creating a TeXML Application](#step-4-create-a-texml-application) - [Buying a phone number](#step-5-buy-a-phone-number) - [Creating your agents' SIP Connections](#step-6-create-your-agents-credentials-based-sip-connections) - [Creating an Outbound Voice Profile to associate with agents' SIP Connections](#step-7-create-an-outbound-voice-profile-and-associate-all-the-sip-connections) - [Setting up your virtual environment](#step-8-set-up-virtual-your-virtual-environment) - [Running application setup and configuring variables](#step-9-set-up-and-configure-variables) - [Running the application](#step-10-running-the-application) - [Optional: Configuring your hangup behavior with TeXML](#configure-your-hangup-behavior) - [Optional: Configuring custom audio playback](#play-custom-audio-files) - [Optional: Configuring voicemail recording and storage](#configure-voicemail-recording-and-storage) * * * * * ## Configuring the Mission Control Portal ### Step 1: Create a Telnyx mission control portal account This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using Call Control. ### Step 2: Create a Telnyx API key >API keys allow your application to access the telephony resources associated with your Telnyx account. In this example, your API key allows the locally-running call center app to access __numbers__, __SIP connections__, __outbound voice profiles__, and TeXML applications that you'll create in the Mission Control Portal during this tutorial. Learn more about [using Telnyx API keys for authentication](/development/api-fundamentals/authentication). Keys can be created in the API Keys section of the portal. ### Step 3: Install and run ngrok >In this example, ngrok is used to receive webhooks sent from the TeXML application to your locally running call center application via a tunneling URL to a port on your machine. These webhooks inform the local application about events like incoming and answered calls. Learn more about [using ngrok with Telnyx](/development/development-tools/ngrok-setup/index#ngrok). Download and install ngrok following the developer's instructions from https://ngrok.com/download. Start up ngrok by running `./ngrok http 8080` and make note of the HTTPS Forwarding URL. ### Step 4: Create a TeXML application >TeXML is the quickest way to build programmable voice applications in minutes using a simple XML data structure. Learn more about TeXML in our [tutorial](/docs/voice/programmable-voice/texml-setup). Add a new TeXML Application in the Call Control section of the Mission Control Portal, selecting Add a new TeXML Application in the top navigation menu. Set the __Voice Method__ to `GET` and specify your webhook URL as `your_ngrok_forwarding_url/TeXML/inbound` - e.g. `https://b06b087392cd.ngrok.io/TeXML/inbound` Set the __Status Callback Method__ to `POST` and specify your callback URL as `your_ngrok_forwarding_url/TeXML/events` - e.g. `https://b06b087392cd.ngrok.io/TeXML/events` ![TeXML APP](https://github.com/team-telnyx/demo-python-telnyx/raw/master/call-center-texml/imgs/texml_app.png) ### Step 5: Buy a phone number Use the Search & Buy Numbers tool in the Mission Control Portal to find a voice-enabled phone number and add it to your cart. At checkout, use the drop-down box labeled __Connection or Application__ to select the TeXML application you just created. This associates your new phone number with the application. This is your call center phone number that users will call to reach your organization. ![buy a phone number](https://github.com/team-telnyx/demo-python-telnyx/raw/master/call-center-texml/imgs/buy_phone_number.png) ### Step 6: Create your agents' Credentials-based SIP Connections Add a new SIP Connection from the SIP Connections section of the Mission Control Portal. Select __Credentials__ as the SIP Connection Type. It's good practice to set the username to be something unique and representative of the agent who will be assigned to the connection. Your connections need to send webhooks to inform your TeXML application about the status of calls so that the application will stop trying to dial other connections when one connection answers an incoming call. Under __Events__, specify the __Webhook URL__ as `your_ngrok_forwarding_url/outbound/event`.  ![Outbound event url](https://github.com/team-telnyx/demo-python-telnyx/raw/master/call-center-texml/imgs/outbound_events.png) Hit __Save & Finish Editing__ to save your progress. When a user calls your call center number, the TeXML application forwards the call to each of the SIP connections associated with the application. Because all of the connections we're creating use SIP URIs instead of phone numbers, these connections need to be able to receive SIP URI calls. Find the connection you just created in the connections list and open the __Inbound Options__ menu. In the Inbound section, set __Recieve SIP URI Calls__ to __From anyone__. ![SIP connection uri](https://github.com/team-telnyx/demo-python-telnyx/raw/master/call-center-texml/imgs/enable_sip_uri.png) Lastly, if your agents will be making outbound calls, you may want to enable a __caller ID override__, which enables Telnyx to send a specified caller ID for each agent. This setting can be found under the __Outbound__ section of the SIP Connection Options menu. ![Caller ID](https://github.com/team-telnyx/demo-python-telnyx/raw/master/call-center-texml/imgs/caller_id.png) Repeat this step to create a new connection for each agent you wish to connect to the call center. Telnyx is fully compatible with every major free softphone platform, with in-depth [configuration guides](/docs/voice/sip-trunking/configuration-guides) for each one. > Don't have a SIP desk phone or softphone to use with this demo? Why not try __WebRTC__? Load up our free WebRTC development demo tool and enter your SIP connection credentials to start making and receiving calls directly from your browser. ### Step 7: Create an Outbound Voice Profile and associate all the SIP Connections Outbound calls must be enabled for the TeXML Application to forward incoming calls to your agents' SIP Connections. Outbound calls are configured with an __Outbound Voice Profile__, which is in turn associated with each SIP Connection to enable calls to be forwarded to that connection.  Add a new profile from the Outbound Voice Profiles section of the portal, then hit __Add connections/apps to profile__ and select each of the connections you created in the previous step. Outbound Voice Profiles allow outbound calls to be placed within the United States and Canada by default. You can enable outbound calling to international destinations in the __International Allowed Destinations__ menu when configuring an Outbound Voice Profile. ## Configuring your environment and running the application ### Step 8: Set up virtual your virtual environment The sample application requires Python version 3.6 or higher and leverages the `aiohttp`, `apscheduler`, and `python-dotenv` packages. You can install these packages manually using `pip`, or use a packaging tool like `pipenv` to install them automatically inside a virtual environment. To install `pipenv`, run: ```bash pip install pipenv ``` After pasting the above content, Kindly check and remove any new line added To use `pipenv` to install the required packages, navigate to the `/call-center-texml` directory, and run: ```bash pipenv shell pipenv update ``` After pasting the above content, Kindly check and remove any new line added This will create the environment and install the application requirements as defined in the Pipfile. ### Step 9: Set up and configure variables The sample application interfaces with the Telnyx resources set up via the Mission Control portal by passing a set of environment variables, including your API key and other unique identifiers, into the app. Setting up the app involves running a setup script that creates a .env file, into which you can populate these environment variables. In the `/call-center-texml` directory, run: ```bash python setup.py ``` After pasting the above content, Kindly check and remove any new line added This will create a `.env` file within the `/call-center-texml/call-center` directory. Open this `.env` file and fill in the required variables: - `API_KEY`: This is your Telnyx API Key, created in [Step 2](#step-2-create-a-telnyx-api-key). - `PROD`: Defaults to __True__. You can set this to either True or False. If set to True, scheduled jobs for updating connections and sending account balance notifications will run at automated time intervals. - `SLACK_URL`: If you wish to integrate your call center with Slack to receive live notifications for incoming calls, this URL will be configured for incoming webhooks in the Slack app. More on setting up Slack API integrations can be found in Slack's documentation. - __Note:__ The `SLACK_URL` can be left blank if Slack is not being used. - `NGROK_URL`: This is the Forwarding URL from [Step 3](#step-3-install-and-run-ngrok). - `OUTBOUND_PROFILE_ID`: This is the ID of the Outbound Voice Profile from [Step 7](#step-7-create-an-outbound-voice-profile-and-associate-all-the-sip-connections). The ID can be found and copied by opening the configuration settings of the profile. Save this file. If these are correct, you should now have everything you need to run the application. ### Step 10: Running The Application From the `/call-center-texml` directory, run the following command to start the application: ```bash PYTHONPATH=`pwd`/ python call_center/main.py ``` After pasting the above content, Kindly check and remove any new line added You will now see the application running on localhost port 8080. You can test your call center application by dialing the number you purchased in [Step 5](#step-5-buy-a-phone-number). The TeXML application will answer the call and inform the caller that they are now attempting to connect them to an available agent. At this point, the clients the agents used to register their SIP Connections credentials will start to ring if they are available. ## Optional: Customizing your call center app ### Configure your hangup behavior When a call is answered and subsequently ended by an agent, the customer hears a text-to-speech message thanking them for calling. Like any other behavior in the call flow, this behavior can be configured by modifying the relevant TeXML files. This particular behavior is specified in the answered.xml file, located in the `/call_center/infrastructure/TeXML/ directory`. The default behavior uses a [``](/docs/voice/programmable-voice/texml-verbs/say) verb in this file to speak a text string. If you have an IVR, you may instead wish to use a [``](/docs/voice/programmable-voice/texml-verbs/dial) or [``](/docs/voice/programmable-voice/texml-verbs/redirect) verb, which could send the caller to another line or back to the IVR, should they wish to have a conversation with another department. ### Play custom audio files The TeXML files are configured to use text-to-speech by default, using `` verbs to communicate information to the user. However, the application is also capable of delivering audio files for initial greetings and hold messages, using the [``](/docs/voice/programmable-voice/texml-verbs/play) verb. All you need to do is record the audio and place the resultant files in a new subdirectory under `/call_center/infrastructure/audio`, named: - `support_greeting.mp3` - `support_busy.mp3` Then un-comment the `` verbs that are in the `busy_template.xml` and `inbound_template.xml` files, and remove or comment-out the `` verbs to prevent the file from reading text-to-speech directly after playing your audio file: ```xml {ngrok_url}/TeXML/support_greeting ``` After pasting the above content, Kindly check and remove any new line added >There's no need to change the `{ngrok_url}` placeholder in the above example - this is populated at runtime from the environment variables you set up in [Step 9](#step-9-set-up-and-configure-variables). ### Configure voicemail recording and storage You can also specify a recording status callback URL in the `voicemail.xml` file. When a call ends after being sent to voicemail, the TeXML application sends a `POST` request with the URL of the recording file to the status callback URL you specified. ## Where to Next? Now that you've set up a fully-functioning, deeply customizable call center application using TeXML, the possibilities are endless:  - Read the story of how we built a bespoke call center for our 24/7 technical support team using TeXML, in our two-part blog series. - Check out the [full TeXML documentation](/docs/voice/programmable-voice/texml-twiml-compatibility) for a list of commands that can be used in your XML files. - Check out a video walkthrough of this tutorial. - Learn more about Telnyx for Contact Centers . --- ### Call Tracking > Source: https://developers.telnyx.com/docs/voice/programmable-voice/call-tracking.md | [Python](#python) | [Node](#node) | ---- ## Python __⏱ 60 minutes build time.__ __🧰 Clone the sample application from our GitHub repo__ In this tutorial, you'll learn how to build a __Call Tracking__ application using the __Telnyx API__, and our __Python SDK__. Call Control (the Telnyx Voice API), combined with our Numbers API, provides everything you need to build a robust number ordering and call tracking application: - The Numbers API enables you to search the Telnyx phone number inventory in real time; filtering by Area Code, City/State, and more to find the perfect local number for your use-case. - Call Control enables you to quickly setup dynamic forwarding numbers, toggle dual-channel recording, join/leave dynamic conferences, and pull post-call analytics. By following this tutorial, you'll build an app that can: > 1. Search and order phone numbers by a city and state combination. > 2. Receive inbound calls to the Telnyx phone number. > 3. Transfer calls using Call Control to your designated Forwarding Number. > 4. Store all required information in a database of your choice. > 5. Make a front-end that shows what's going on. ### Create a Telnyx mission control portal account To get started, you'll need to create an account. Verify your email address and you can log into the Mission Control Portal to get started. ### Set up your local machine to receive webhooks from Telnyx Next, you'll need a means of receiving webhooks sent by Telnyx to notify your application of call events. One of the easiest ways to accomplish this is to [use a tool like ngrok](/development/development-tools/ngrok-setup/index#ngrok) to generate a tunnelling URL, which connects to a locally running application via a port on your machine. In this example, port `8000` is used. After downloading and installing ngrok, run `./ngrok http 8000` and make note of the resultant __HTTPS Forwarding URL__. ### Create a Telnyx call control application From the Portal, create a new Call Control Application , and paste the __HTTPS Forwarding URL__ from the previous steps to send webhooks from this application to your local machine via ngrok. Ensure API v2 is selected, and save your application. We don't need to worry about any other application settings for now. Select your application again to edit it, and make a note of the __ID__. This is how you'll identify your Call Control Application in your code. ### Create an Outbound Voice profile From the Portal, create a new Outbound Voice Profile. Click __Add connections/apps to profile__ and select the Call Control Application you created in the previous step. In the __International Allowed Destinations__ section, ensure you have selected the region(s) in which you want your application to work. ### Initialize and install packages via pip Initialize your call tracking application with the defaults presented to you and create a virtual environment. ```bash mkdir call-tracking cd call-tracking python3 -m venv /path/to/new/virtual/environment ``` After pasting the above content, Kindly check and remove any new line added Then install the necessary packages for the call tracking application. They can be found in this Pipfile or manually install them: ```bash pip install flask pip install flask-modus pip install python-dotenv pip install telnyx pip install peewee pip install pymysql pip install werkzeug==0.16.1 ``` After pasting the above content, Kindly check and remove any new line added Brief explanation on the required packages: Flask: ### Set up environment variables The following environment variables need to be set for your call tracking application to work: Variable Description TELNYX_API_KEY Your Telnyx API Key, which can be created in the portal. TELNYX_PUBLIC_KEY Your Telnyx Public Key, which is accessible via the portal. TELNYX_CONNECTION_ID The ID from your Call Control Application MESSAGING_PROFILE_ID The ID from your Messaging Profile DATABASE_HOST Connection of the host (ie. localhost or your local ip address) DATABASE_USER Your database user name DATABASE_PASSWORD Your database password DATABASE_NAME Your database name DATABASE_PORT Your database port This app uses the excellent dotenv package to manage environment variables. Make a copy of the file below, add your credentials, and save as `.env` in the root directory. ```bash TELNYX_API_KEY="YOUR_API_KEY" TELNYX_CONNECTION_ID="YOUR_CALL_CONTROL_ID" MESSAGING_PROFILE_ID="YOUR_MESSAGING_PROFILE_ID" DATABASE_HOST="localhost" DATABASE_USER="root" DATABASE_PASSWORD="" DATABASE_NAME="cctracker" DATABASE_PORT="" ``` After pasting the above content, Kindly check and remove any new line added ### Create some folders and Python files to build our call tracking application We'll use a few `.py` files to build the call tracking application. * `app.py` as our entry point to the application * `database.py` for our database * `database_queries.py` for our database controller * `telnyx_commands.py` to manage most of our telnyx related functions We would also like to categorize and sort these in a practical sense, so we are going to be making a few folders to sort the files into: * `model` to host our databse related quieries * `static` for our css and js * `templates` as our entry point to everything html and frontend that we would want So let's create our folders and files: ```bash mkdir model mkdir static mkdir templates touch app.py touch telnyx_commands.py touch model/database.py touch model/database_queries.py ``` After pasting the above content, Kindly check and remove any new line added This then should create the two files in our model directory, and two files in our base directory to get started ### Setup basic Telnyx commands Here we will setup some basic commands to get ourselves going for the call tracking app. We will want the ability to procure some numbers via the API, have the capability to delete them, and look up some basic CNAM paramaters if we can. As such, we will be creating some basic functions: * telnyx_number_acquire(locality, administrative_area): This will handle the number search and ordering portion of our app when given the specific arameters - We will be specifying locality and rate_center which corresponds with City and State. - We will also go ahead and search for numbers that are SMS capable so we can future proof just in case we would want to be adding on an SMS component to this. - Setting limit as 1 to fetch and procure the first result - Making sure quickship is set as True, so we get numbers that are actively ready to go out of the box and will not have to wait for procurement. - We will want to return the `number_to_order` and `city_state_combo` to pass which number and from where exactly we procured this from * telnyx_number_delete(number_to_delete): This will handle deleting phone numbers in our portal * telnyx_cnam_lookup(calling_number): This will handle using Telnyx Lookup service to see if we can get information on the number that's calling us - We will be returning the variable `cnam_info` with the result to use later on * difference(start_time, end_time): This handles conversion of the webhook start/end times to get call durations - Webhook times are in full time format, so we will use the included datetime function to convert the time into seconds before doing the math to get the difference for the duration - We will be returning both `duration` and `date` ```python // In app.py import telnyx import os import math from flask import redirect, url_for, flash from datetime import datetime def telnyx_number_acquire(locality, administrative_area): city_state_combo = locality + ", " + administrative_area number_search = telnyx.AvailablePhoneNumber.list(filter={ "locality": locality, "rate_center": administrative_area, "features": "sms", "limit": "1", "quickship": True, }) # catch no result error if number_search.metadata.total_results != 1: flash("No results found for specified area, " "try again! Watch our for typos!") return redirect(url_for('index')) else: number_to_order = number_search.data[0]["phone_number"] number_order_response = telnyx.NumberOrder.create( phone_numbers=[ {"phone_number": number_to_order, }, ], messaging_profile_id=os.getenv("MESSAGING_PROFILE_ID"), connection_id=os.getenv("TELNYX_CONNECTION_ID"), ) return number_to_order, city_state_combo def telnyx_number_delete(number_to_delete): retrieve = telnyx.PhoneNumber.retrieve(number_to_delete) retrieve.delete() def telnyx_cnam_lookup(calling_number): resource = telnyx.NumberLookup.retrieve(calling_number) if resource.caller_name is None: cnam_info = "Not Available" return cnam_info else: cnam_info = resource.caller_name return cnam_info # date and time difference function def difference(start_time, end_time): end_time = ''.join(end_time) start_time = ''.join(start_time) d1 = datetime.strptime(end_time, '%Y-%m-%dT%H:%M:%S.%fZ') d2 = datetime.strptime(start_time, '%Y-%m-%dT%H:%M:%S.%fZ') d3 = d1 - d2 d4 = d3.total_seconds() duration = math.ceil(d4) date = d2.date() return duration, date ``` After pasting the above content, Kindly check and remove any new line added ### Database and database queries setup We will need to now setup our database and store some of this data that we will be getting. You can setup a basic database in-memory, but obviously this results with the drawback of it being killed every time the app is restarted. As such, I've personally chosen to use Oracle SQL. I believe a relational database makes the most sense in this case to use, as we are relating tracking inbound numbers that are calling us with forwarded phone numbers. ie. all the data that would be presented is tied to the same call/number combination. So for this we will be creating two files: * `database.py`: to setup and create our basic database * `database_queries.py`: to provide all the functions we would need related to our database #### in database.py - We will be using peewee to connect to our database - We will then define our table classes and add a function to create them at the end ```python import os from dotenv import load_dotenv from peewee import * from peewee import CharField load_dotenv() mysql_db = MySQLDatabase(os.getenv('DATABASE_NAME'), user=os.getenv('DATABASE_USER'), password=os.getenv('DATABASE_PASSWORD'), host=os.getenv('DATABASE_HOST'), port=int(os.getenv('DATABASE_PORT'))) # Database setup # inheritance for Meta (peewee), assigns DB to subsequent DB classes class BaseModel(Model): class Meta: database = mysql_db # peewee constructs id primary keys automatically (they are required to make queries) class CallTracker(BaseModel): from_cnam_lookup = CharField() from_number = CharField() purchased_number = CharField() forward_number = CharField() date = CharField() duration_of_call = CharField() class ForwardedPhoneNumbers(BaseModel): purchased_number = CharField() city_state = CharField() forward_number = CharField() tag = TextField() # Create tables function if __name__ == "__main__": mysql_db.connect() mysql_db.create_tables([CallTracker, ForwardedPhoneNumbers]) print('Created tables! (or they already exist)') ``` After pasting the above content, Kindly check and remove any new line added Now, if we were to run `database.py` (if you were to pass the correct dot.env variables related to database login), you should successfully be able to create the tables in your desired database. For MySQL, do make sure your database schema is created first and matches your DATABASE_NAME parameter, for example in MySQL Workbench ### Setup Flask Server for Number Ordering and Call Tracking The `app.py` file sets up 5 routes: * `/` : Our base route, where we will have our interface once we construct our index.html * `/number` : To manage our number ordering and patching that we will be setting up * `/call` : This path will relate to our call logging service that we will show. We will need to hit this if we would like to delete certain calls * `/call-control/inbound` : This points to our main call-control processing * `/call-control/outbound` : To manage our number ordering and patching that we will be setting up We will be sending POST/PATCH/GET/DELETE requests to these endpoints. Take note that Flask only natively supports POST/GET requests. This is the reason we will be using Flask-modus to override the methods. This allows us to be more secure and hit endpoints that make sense, preventing irregularities such as hitting a GET endpoint and that resulting in the deletion of data that you intended to keep. ```python import telnyx import os import json from dotenv import load_dotenv from flask import Flask, \ render_template, request, Response, redirect, url_for, flash from flask_modus import Modus from urllib.parse import urlunsplit from model.database_queries import db_fetch_data, \ db_number_insert, db_number_update, db_number_row_identifier, \ db_number_delete, db_call_delete, db_number_forward_fetch, \ db_call_insert from telnyx_commands import telnyx_number_acquire, \ telnyx_number_delete, telnyx_cnam_lookup, difference load_dotenv() app = Flask(__name__) modus = Modus(app) app.secret_key = "SecretKey" # homepage @app.route('/') def index(): all_phone_numbers, all_call_data = db_fetch_data() return render_template('index.html', all_phone_numbers=all_phone_numbers, all_call_data=all_call_data, ) # search and order first number we get based on City/State @app.route("/number/", methods=['POST']) def acquire(): # pull data to store in db later to display on frontend locality = request.form["city"] administrative_area = request.form["state"] forward_number = request.form["forward_number"] tag = request.form["tag"] city_state_combo = locality + ", " + administrative_area number_to_order, city_state_combo = telnyx_number_acquire(locality, administrative_area) db_number_insert(number_to_order, city_state_combo, forward_number, tag) flash("Phone Number:" + number_to_order + " Was Purchased Successfully!") return redirect(url_for('index')) # using modus module to incorporate PATCH and DELETE requests @app.route("/number//", methods=['PATCH', 'DELETE']) def update(id): try: if request.method == b'PATCH': # grabbing id from index id = request.form.get('id') # updating new variables in update screen updated_forward_number = request.form["forward_number"] updated_tag = request.form["tag"] phone_number = db_number_update(id, updated_forward_number, updated_tag) flash("Phone Number" + phone_number + " Was Updated Successfully") elif request.method == b'DELETE': number_to_delete = db_number_row_identifier(id) # delete from telnyx portal telnyx_number_delete(number_to_delete) # delete from database and save db_number_delete(id) flash("Phone Number" + number_to_delete + " Successfully Deleted") except Exception as e: print("Error updating database") print(e) return redirect(url_for('index')) @app.route("/call//", methods=['DELETE']) def delete_call(id): if request.method == b'DELETE': db_call_delete(id) flash("Call Record Successfully Deleted!") return redirect(url_for('index')) def handle_call_answered(call, called_number): number_to_forward_to = db_number_forward_fetch(called_number) webhook_url = urlunsplit(( request.scheme, request.host, "/call-control/outbound", "", "")) transfer_params = { "to": number_to_forward_to, "webhook_url": webhook_url } call.transfer(**transfer_params) @app.route("/call-control/inbound", methods=["POST"]) def inbound_call(): # store some id values JUST IN CASE for troubleshooting purposes body = json.loads(request.data) calling_number = body["data"]["payload"]["from"] called_number = body["data"]["payload"]["to"] payload = call_control_id = body["data"]["payload"] call_control_id = body["data"]["payload"]["call_control_id"] call_session_id = body["data"]["payload"]["call_session_id"] call_leg_id = body["data"]["payload"]["call_leg_id"] event_type = body["data"]["event_type"] webhook_url = urlunsplit(( request.scheme, request.host, "/call-control/outbound", "", "")) # construct call object, which is needed for initial call control commands call = telnyx.Call() call.call_control_id = call_control_id # main logic response based on inbound webhook events try: if event_type == "call.initiated": call = telnyx.Call(connection_id=os.getenv("TELNYX_CONNECTION_ID")) call.call_control_id = body.get("data").get("payload").get("call_control_id") call.answer() print(calling_number) print(called_number) elif event_type == "call.answered": handle_call_answered(call, called_number) elif event_type == "call.hangup": print(body) cnam_info = telnyx_cnam_lookup(calling_number) # time difference end_time = ''.join(body.get("data").get("payload").get("end_time")) start_time = ''.join(body.get("data").get("payload").get("start_time")) duration, date = difference(start_time, end_time) forward_number = db_number_forward_fetch(called_number) db_call_insert(cnam_info, calling_number, called_number, forward_number, date, duration) except Exception as e: print("Error processing webhook") print(e) return Response(status=200) @app.route("/call-control/outbound", methods=["POST"]) def outbound_call(): body = json.loads(request.data) call_leg_id = body[ "data"][ "payload"][ "call_leg_id"] print(f"Received call_control event with call_leg_id: {call_leg_id}") return Response(status=200) if __name__ == "__main__": telnyx.api_key = telnyx.os.getenv("TELNYX_API_KEY") TELNYX_APP_PORT = "8000" app.run(port=TELNYX_APP_PORT) ``` After pasting the above content, Kindly check and remove any new line added Here we are importing our functions from the other files we made and constructing our routes with specific methods to perform those functions. I want to focus specifically on the `/call-control/inbound` route. Here we are performing the function of parsing through the incoming webhooks that we will be getting into our application, specifically: * Receiving inbound call webhooks * [Answering the inbound call](/api-reference/call-commands/answer-call) * [Transferring the call](/api-reference/call-commands/transfer-call#transfer-call) to the destination number saved in the database * Saving the [hangup event](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup) to the database We also would like to save a good majority of the information from above. If you ever suffer problems from call quality/something not working, providing our NOC team the call_control_id/call_session_id will expedite the process of resolution to your inquiry. ### Building the front-end The front-end was built with Boostrap and Nunjucks. I won't go into much detail about building it out in this article, but if you want to attach your methods from above simply import the resources located in the static and templates folders located on our GitHub page. ### Running the call tracking application We should now be able to run the application! ### Launch ngrok and update your call control application We need to be able to receive webhooks from Telnyx, sent over the public Internet. We'll use [ngrok](/development/development-tools/ngrok-setup/index#ngrok) for this tutorial. Launch ngrok on the `PORT` specified in your `.env` file. If you're using port `8000` (the default for this app), you can simply run `./ngrok http 8000` ```bash $ ./ngrok http 8000 ngrok by @inconshreveable Session Status online Account Little Bobby Tables (Plan: Free) Version 2.x.x Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://ead8b6b4.ngrok.io -> localhost:8000 Forwarding https://ead8b6b4.ngrok.io -> localhost:8000 Connections ttl opn rt1. rt5 p50 p90 0 0 0.00 0.00 0.00 0.00 ``` After pasting the above content, Kindly check and remove any new line added Once you've set up ngrok (or another tunneling service of your choice) you can add the public proxy URL to your Inbound Settings in the Mission Control Portal. To do this, click the edit symbol \[__✎__\] next to your Call Control Application In the __Send a webhook to the URL:__ field, paste the forwarding address from ngrok into the Webhook URL field. Add `/call-control/inbound` to the end of the URL to direct the request to the webhook endpoint in your server. If we were using the example URL from the code sample above, the URL would be `http://ead8b6b4.ngrok.io/call-control/inbound`. ### Run the APP.PY Call tracking application Start the server by running `python app.py`. Once everything is setup, you should now be able to: * Search and purchase a number based on your parameters * Allocate the purchased number to your desired forwarding number * Track your acquired forwarded phone numbers in your database * Record and store call information relating to those numbers in your database * Present all of this information in your UI ### Call tracking follow-ons Now that you've successfully constructed this application, you have the freedom to expand it as you wish! You can start saving even more information from the webhooks such as IDs in your database by adding more tables, you can add more routes to handle inbound messaging functions, you can add recording/auto answer functions... it's all up to you! Our developer Slack community is full of Python developers like you - be sure to join to see what your fellow developers are building! ## Node __⏱ 60 minutes build time.__ __🧰 Clone the sample application from ourGitHub repo__ In this tutorial, you'll learn how to build a __Call Tracking__ application using the __Telnyx API__, and our __Node SDK__. Programmable Voice, combined with our Numbers API, provides everything you need to build a robust call tracking application: - The Numbers API enables you to search the Telnyx phone number inventory in real time; filtering by Area Code, City/State, and more to find the perfect local number for your use-case. - Call Control enables you to quickly setup dynamic forwarding numbers, toggle dual-channel recording, join/leave dynamic conferences, and pull post-call analytics. By following this tutorial, you'll build an app that can: > 1. Search and order a phone number by area code. > 2. Store a 'binding' of Telnyx phone numbers to a forwarding number (to which incoming calls to the Telnyx phone numbers will be forwarded). > 3. Receive inbound calls to the Telnyx phone number. > 4. Transfer calls using Call Control. > 5. Store webhook events associated with calls to a datastore. ### Create a Telnyx mission control portal account This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using Call Control. ### Set up your local machine to receive webhooks from Telnyx One of the easiest ways to accomplish this is to [use at tool like ngrok ](/development/development-tools/ngrok-setup/index#ngrok) to generate a tunnelling URL, which connects to a locally running application via a port on your machine. In this example, port `8000` is used. After downloading and installing ngrok, run `./ngrok http 8000` and make note of the resultant __HTTPS Forwarding URL__. ### Create a Telnyx call control application From the Portal, create a new Call Control Application , and paste the __HTTPS Forwarding URL__ from the previous steps to send webhooks from this application to your local machine via ngrok. Ensure API v2 is selected, and save your application. We don't need to worry about any other appliction settings for now. Select your application again to edit it, and make a note of the __ID__. This is how you'll identify your Call Control Application in your code. ### Create an outbound voice profile From the Portal, create a new Outbound Voice Profile. Click __Add connections/apps to profile__ and select the Call Control Application you created in the previous step. In the __International Allowed Destinations__ section, ensure you have selected the region(s) in which you want your application to work. ### Initialize and Install packages via npm Initialize your call tracking application with the defaults presented to you. ```bash mkdir call-tracking cd call-tracking npm init ``` After pasting the above content, Kindly check and remove any new line added Then install the necessary packages for the call tracking application ```bash npm i dotenv npm i express npm i telnyx ``` After pasting the above content, Kindly check and remove any new line added This will create a `package.json` file with the packages needed to run the application. ### Set up environment variables The following environment variables need to be set for your call tracking application to work: Variable Description TELNYX_API_KEY Your Telnyx API Key, which can be created in the portal. TELNYX_PUBLIC_KEY Your Telnyx Public Key, which is accessible via the portal. TELNYX_CONNECTION_ID The ID from your Call Control Application PORT The port through which the app will be served. This variable defaults to 8000 This app uses the excellent dotenv package to manage environment variables. Make a copy of the file below, add your credentials, and save as `.env` in the root directory. ```bash TELNYX_PUBLIC_KEY= TELNYX_API_KEY= TELNYX_CONNECTION_ID= PORT=8000 ``` After pasting the above content, Kindly check and remove any new line added ### Create JavaScript files to build a Call Tracking Application We'll use a few `.js` files to build the call tracking application. * `index.js` as our entry point to the application * `db.js` for our database controller (in-memory DB for sample) * `callControl.js` to manage call-control webhooks * `bindings.js` to manage call-tracking bindings and post-call metadata ```bash touch index.js touch db.js touch callControl.js touch bindings.js ``` After pasting the above content, Kindly check and remove any new line added ### Setup Express Server for Call Tracking The `index.js` file sets up 2 express routes: * `/call-control` : To handle call-control webhooks * `/bindings` : To manage phone number bindings and call information ```js // In index.js import 'dotenv/config'; dotenv.config(); import express from 'express'; import bindings from './bindings'; import callControl from './callControl'; const app = express(); app.use(express.json()); app.use(express.urlencoded({extended: true})); const callControlPath = '/call-control'; app.use(callControlPath, callControl); const bindingsPath = '/bindings' app.use(bindingsPath, bindings); app.listen(process.env.TELNYX_APP_PORT); console.log(`Server listening on port ${process.env.TELNYX_APP_PORT}`); ``` After pasting the above content, Kindly check and remove any new line added ### Setup database for Call Tracking information The `db.js` file contains the in-memory database to manage our phone numbers and call information. It exports 1 array and 3 functions: * `bindings = []` : Our in-memory database * `addPhoneNumberBinding` : accepts a Telnyx phone number and a destination number to save to the database. * Called when ordering / creating a new call-tracking number * `getDestinationPhoneNumber` : accepts a Telnyx phone number and searches the database for a match, then returns the destination phone number. * Called when receiving an inbound call to look up transfer destination. * `saveCall` : accepts a Telnyx event and saves the call to the database based on the payload. * Called when the `call.hangup` event is received to save post-call information * `getBinding`: accepts a Telnyx phone number and returns the matching binding information from the database. * Called when `GET` bindings has a telnyxPhoneNumber query parameter ```javascript // in db.js export const bindings = []; export const addPhoneNumberBinding = (telnyxPhoneNumber, destinationPhoneNumber) => { const index = bindings.findIndex(binding => binding.telnyxPhoneNumber === telnyxPhoneNumber); if (index > 0) { return { ok: false, message: `Binding of Telnyx: ${telnyxPhoneNumber} already exists`, binding: bindings[index] } } const binding = { telnyxPhoneNumber, destinationPhoneNumber, calls: [] } bindings.push(binding); return { ok: true } }; export const getDestinationPhoneNumber = telnyxPhoneNumber => { const destinationPhoneNumber = bindings .filter(binding => binding.telnyxPhoneNumber === telnyxPhoneNumber) .reduce((a, binding) => binding.destinationPhoneNumber, ''); return destinationPhoneNumber; }; export const saveCall = callWebhook => { const telnyxPhoneNumber = callWebhook.payload.to; const index = bindings.findIndex( binding => binding.telnyxPhoneNumber === telnyxPhoneNumber); bindings[index].calls.push(callWebhook); }; export const getBinding = telnyxPhoneNumber => { return bindings.filter( binding => binding.telnyxPhoneNumber === telnyxPhoneNumber); }; ``` After pasting the above content, Kindly check and remove any new line added ### Managing phone number bindings for Call Tracking The `bindings.js` file contains all the logic for: * [Searching Phone Numbers](/api-reference/phone-number-search/list-available-phone-numbers) by area code (also known as `national_destination_code`) * [Ordering Phone Numbers](/api-reference/phone-number-orders/create-a-number-order) and setting the `connection_id` as part of the order * Saving the binding to the database * Routes for fetching binding information ```javascript // in bindings.js import express from 'express'; import Telnyx from 'telnyx'; import db from './db'; const telnyx = new Telnyx("YOUR_API_KEY"); export const router = express.Router(); const CONNECTION_ID = process.env.TELNYX_CONNECTION_ID; const searchNumbers = async (req, res, next) => { const isInvalidRequest = (!req.body.areaCode || !req.body.destinationPhoneNumber || req.body.areaCode.length !== 3) if (isInvalidRequest) { res.send({ message: 'Invalid search criteria, please send 3 digit areaCode', example: '{ "areaCode": "919", "destinationPhoneNumber": "+19198675309" }' }); return; } try { const areaCode = req.body.areaCode; const availableNumbers = await telnyx.availablePhoneNumbers.list({ filter: { national_destination_code: areaCode, features: ["sms", "voice", "mms"], limit: 1 } }); const phoneNumber = availableNumbers.data.reduce((a, e) => e.phone_number, ''); if (!phoneNumber) { res.send({message: 'No available phone numbers'}).status(200); } else { res.locals.phoneNumber = phoneNumber; next(); } } catch (e) { const message = '' console.log(message); console.log(e); res.send({message}, ...e).status(400); } } const orderNumber = async (req, res, next) => { try { const phoneNumber = res.locals.phoneNumber; const result = await telnyx.numberOrders.create({ connection_id: CONNECTION_ID, phone_numbers: [{ phone_number: phoneNumber }] }); res.locals.phoneNumberOrder = result.data; next(); } catch (e) { const message = `Error ordering number: ${res.locals.phoneNumber}` console.log(message); console.log(e); res.send({message}, ...e).status(400); } } const saveBinding = async (req, res) => { try { const telnyxPhoneNumber = res.locals.phoneNumber; const destinationPhoneNumber = req.body.destinationPhoneNumber; db.addPhoneNumberBinding(telnyxPhoneNumber, destinationPhoneNumber); res.send(res.locals.phoneNumberOrder); } catch (e) { res.send(e).status(409); } } const getBindings = async (req, res) => { if (req.query.telnyxPhoneNumber) { const telnyxPhoneNumber = req.query.telnyxPhoneNumber; const binding = db.getBinding(telnyxPhoneNumber); res.send(binding).status(200); } else { res.send(db.bindings); } } router.route('/') .post(searchNumbers, orderNumber, saveBinding) .get(getBindings); ``` After pasting the above content, Kindly check and remove any new line added ### Managing call flows for call tracking The `callControl.js` file contains the routes and functions for: * Receiving inbound call webhooks * [Answering the inbound call](/api-reference/call-commands/answer-call) * [Transferring the call](/api-reference/call-commands/transfer-call#transfer-call) to the destination number saved in the database * Saving the [hangup event](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup) to the database ```js // in callControl.js import express from 'express'; import Telnyx from 'telnyx'; import db from './db'; const telnyx = new Telnyx("YOUR_API_KEY"); export const router = express.Router(); const outboundCallController = async (req, res) => { res.sendStatus(200); // Play nice and respond to webhook const event = req.body.data; const callIds = { call_control_id: event.payload.call_control_id, call_session_id: event.payload.call_session_id, call_leg_id: event.payload.call_leg_id } console.log(`Received Call-Control event: ${event.event_type} DLR with call_session_id: ${callIds.call_session_id}`); } const handleInboundAnswer = async (call, event, req) => { console.log(`call_session_id: ${call.call_session_id}; event_type: ${event.event_type}`); try { const webhook_url = (new URL('/call-control/outbound', `${req.protocol}://${req.hostname}`)).href; const destinationPhoneNumber = db.getDestinationPhoneNumber(event.payload.to); await call.transfer({ to: destinationPhoneNumber, webhook_url }) } catch (e) { console.log(`Error transferring on call_session_id: ${call.call_session_id}`); console.log(e); } } const handleInboundHangup = (call, event) => { console.log(`call_session_id: ${call.call_session_id}; event_type: ${event.event_type}`); db.saveCall(event); } const inboundCallController = async (req, res) => { res.sendStatus(200); // Play nice and respond to webhook const event = req.body.data; const callIds = { call_control_id: event.payload.call_control_id, call_session_id: event.payload.call_session_id, call_leg_id: event.payload.call_leg_id } const call = new telnyx.Call(callIds); switch (event.event_type) { case 'call.initiated': await call.answer(); break; case 'call.answered': await handleInboundAnswer(call, event, req); break; case 'call.hangup': handleInboundHangup(call, event); break; default: console.log(`Received Call-Control event: ${event.event_type} DLR with call_session_id: ${call.call_session_id}`); } } router.route('/outbound') .post(outboundCallController); router.route('/inbound') .post(inboundCallController); ``` After pasting the above content, Kindly check and remove any new line added ### Running the Call Tracking application Now that you've saved all the examples and built your routes, it's time to run the application. ### Launch ngrok and update your Call Control Application We need to be able to receive webhooks from Telnyx, sent over the public Internet. We'll use [ngrok](/development/development-tools/ngrok-setup/index#ngrok) for this tutorial. Launch ngrok on the `PORT` specified in your `.env` file. If you're using port `8000` (the default for this app), you can simply run `./ngrok http 8000` ```bash $ ./ngrok http 8000 ngrok by @inconshreveable Session Status online Account Little Bobby Tables (Plan: Free) Version 2.x.x Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://ead8b6b4.ngrok.io -> localhost:8000 Forwarding https://ead8b6b4.ngrok.io -> localhost:8000 Connections ttl opn rt1. rt5 p50 p90 0 0 0.00 0.00 0.00 0.00 ``` After pasting the above content, Kindly check and remove any new line added Once you've set up ngrok (or another tunneling service of your choice) you can add the public proxy URL to your Inbound Settings in the Mission Control Portal. To do this, click the edit symbol \[__✎__\] next to your Call Control Application In the __Send a webhook to the URL:__ field, paste the forwarding address from ngrok into the Webhook URL field. Add `/call-control/inbound` to the end of the URL to direct the request to the webhook endpoint in your server. If we were using the example URL from the code sample above, the URL would be `http://ead8b6b4.ngrok.io/call-control/inbound`. ### Run the Node.JS call tracking application Start the server by running `node index.js`. Once everything is setup, you should now be able to: * Allocate a new call tracking number and bind it to a forwarding number * Call the allocated number and get connected to the destination. ### Create a binding for call tracking The bindings interface is managed through a RESTful API. To create a new binding create a `POST` request to your ngrok URL (in this example: `http://ead8b6b4.ngrok.io/bindings`) The `POST` request accepts a JSON object with the following fields: * `areaCode`: Desired area code for the new call tracking phone number * `destinationPhoneNumber` : Number which we'll forward all incoming calls to the call-tracking phone number ```http POST http://ead8b6b4.ngrok.io/bindings HTTP/1.1 Content-Type: application/json; charset=utf-8 { "areaCode" : "919", "destinationPhoneNumber": "+19198675309" } ``` After pasting the above content, Kindly check and remove any new line added The application will search the Telnyx number inventory for a phone number matching the `areaCode` passed, and will order the first result returned from the API. It then creates a binding so that any inbound call to the Telnyx phone number is forwarded to the destination phone number. ### List call tracking bindings and call information The bindings endpoint supports a `GET` request to pull call information and existing bindings. The bindings object returns a `calls` array with the hangup webhooks saved. The length of the array equals the number of calls the call tracking number received. The duration for each call can be calculated as the difference between the `start_time` and `end_time` values. ```http GET http://ead8b6b4.ngrok.io/bindings HTTP/1.1 HTTP/1.1 200 OK Content-Type: application/json [ { "telnyxPhoneNumber": "+19193234088", "destinationPhoneNumber": "+19198675309", "calls": [ { "event_type": "call.hangup", "id": "cddecb2a-bb3c-4e90-8e85-e1b6d51a901b", "occurred_at": "2021-01-26T16:00:55.413407Z", "payload": { "call_control_id": "v2:GegDKN9TMwSPYwUALiLrqNd-TpfER6QgvvNg49reRPtz6mhrhBiTTg", "call_leg_id": "a704d6e6-5fef-11eb-9e5f-02420a0f7568", "call_session_id": "a704df56-5fef-11eb-9718-02420a0f7568", "client_state": null, "connection_id": "1557657082730120568", "end_time": "2021-01-26T16:00:55.413407Z", "from": "+14154886792", "hangup_cause": "normal_clearing", "hangup_source": "caller", "sip_hangup_cause": "200", "start_time": "2021-01-26T16:00:46.873401Z", "to": "+19193234088" }, "record_type": "event" } ] } ] ``` After pasting the above content, Kindly check and remove any new line added ### Call tracking follow-Ons Now that you've successfully built a call tracking application, you can explore more features and discover ideas to build new applications. Our developer Slack community is full of Node developers like you - be sure to join to see what your fellow developers are building! --- ### Conferencing > Source: https://developers.telnyx.com/docs/voice/programmable-voice/conferencing-demo.md \| [Python](#python) | [PHP](#php) | [Node](#node) | [Ruby](#ruby) | - - - ## Python ⏱ **60 minutes build time || Github Repo** ### Introduction The [Voice API framework](/api-reference/call-commands/dial), previously called Call Control, is a set of APIs that allow complete control of a call flow from the moment a call begins to the moment it is completed. In between, you will receive a number of [webhooks](/docs/voice/programmable-voice/receiving-webhooks) for each step of the call, allowing you to act on these events and send commands using the Telnyx Library. A subset of the operations available in the Voice API is the [Conference](/api-reference/conference-commands/conference-recording-start) API. This allows the user (you) to create and manage a conference programmatically upon receiving an incoming call, or when initiating an outgoing call. The Telnyx Python Library is a convenient wrapper around the Telnyx REST API. It allows you to access and control call flows using an intuitive object-oriented library. This tutorial will walk you through creating a simple Flask and Ngrok server application that allows you to create and manage a conference. ### Setup This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. * make sure the *Webhook API Version* is **API v2** You’ll also need to have `python` installed to continue. You can check this by running the following: ```bash $ python3 -v ``` After pasting the above content, Kindly check and remove any new line added Now in order to receive the necessary webhooks for our IVR, we will need to set up a server. For this tutorial, we will be using Flask, a micro web server framework. A quickstart guide to flask can be found on their official website. For now, we will install flask using pip. ```bash $ pip install flask ``` After pasting the above content, Kindly check and remove any new line added You can get the full set of available Telnyx Voice API Commands [here](/api-reference/call-commands/dial). You can also find the Conference Commands [here](/api-reference/conference-commands/conference-recording-start) For each Telnyx Voice API Command we will be using the Telnyx Python SDK. To execute this API we are using Python `telnyx`, so make sure you have it installed. If not you can install it with the following command: ```bash $ pip install telnyx ``` After pasting the above content, Kindly check and remove any new line added After that you’ll be able to use ‘telnyx’ as part of your app code as follows: ```python import telnyx ``` After pasting the above content, Kindly check and remove any new line added We will also import Flask in our application as follows: ```python from flask import Flask, request, Response ``` After pasting the above content, Kindly check and remove any new line added And set our api key using the Python telnyx SDK: ```python telnyx.api_key = "YOUR_TELNYX_API_KEY" ``` After pasting the above content, Kindly check and remove any new line added ### Server and Webhook setup Flask is a great application for setting up local servers. However, in order to make our code public to be able to receive webhooks from Telnyx, we are going to need to use a tool called ngrok. Installation instructions can be found [here](/development/development-tools/ngrok-setup/index#ngrok). Now to begin our flask application, underneath the import and setup lines detailed above, we will add the following: ```python app = Flask(__name__) @app.route('/webhook', methods=['POST']) def respond(): //Our code for handling the call control application will go here print(request.json[‘data’]) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added This is the base Flask application code specified by their documentation. This is the minimum setup required to receive webhooks and manipulate the information received in json format. To complete our setup, we must run the following to set up the Flask environment (note YOUR_FILE_NAME will be whatever you .py file is named): ```bash $ export FLASK_APP=YOUR_FILE_NAME.py ``` After pasting the above content, Kindly check and remove any new line added Now, we are ready to serve up our application to our local server. To do this, run: ```bash $ flash run ``` After pasting the above content, Kindly check and remove any new line added A successful output log should look something like: ```bash * Serving Flask app "main" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) ``` After pasting the above content, Kindly check and remove any new line added Now that our Flask application is running on our local server, we can use ngrok to make this public to receive webhooks from Telnyx by running the following command wherever the ngrok executable is located (NOTE you may have to open another terminal window or push the Flask process to the background): ```bash $ ./ngrok http 5000 ``` After pasting the above content, Kindly check and remove any new line added Once this is up and running, you should see the output URL in the command logs or located on the ngrok dashboard page. This url is important because it will be where our Voice API Application will be sending webhooks to. Grab this url and head on over to the Telnyx Dashboard page. Navigate to your Voice API Application and add the URL to the section labeled "Send a webhook to the URL" as shown below. Add the ngrok url to that section and we are all set up to start our IVR! **Ensure that you append '/webhook' to the ngrok url as specified in our Flask Application** ![URL Webhook Section](https://images.ctfassets.net/4b49ta6b3nwj/5fWNOgoZnSwcSj28O1B5Ld/f951a6c0b7118f3a27d86aa5d5035d5e/call_control_url_webhook.PNG) ### Receiving and interpreting Webhooks We will be configuring our respond function to handle certain incoming webhooks and execute Voice API commands based on what the values are. Flask catches the incoming webhooks and calls the respond() function every time a webhook is sent to the route we specified as ‘/webhook’. We can see the json value of the hook in the request.json object. Here is what a basic Telnyx Call Object looks like ```json { 'data': { 'event_type': 'call.initiated', 'id': 'a2fa3fa6-4e8c-492d-a7a6-1573b62d0c56', 'occurred_at': '2020-07-10T05:08:59.668179Z', 'payload': { 'call_control_id': 'v2:rcSQADuW8cD1Ud1O0YVbFROiQ0_whGi3aHtpnbi_d34Hh6ELKvLZ3Q', 'call_leg_id': '76b31010-c26b-11ea-8dd4-02420a0f6468', 'call_session_id': '76b31ed4-c26b-11ea-a811-02420a0f6468', 'caller_id_name': '+17578390228', 'client_state': None, 'connection_id': '1385617721416222081', 'direction': 'incoming', 'from': '+14234567891', 'start_time': '2020-07-10T05:08:59.668179Z', 'state': 'parked', 'to': '+12624755500' }, 'record_type': 'event' }, 'meta': { 'attempt': 1, 'delivered_to': 'http://59d6dec27771.ngrok.io/webhook' } } ``` After pasting the above content, Kindly check and remove any new line added We want to first check and see if the incoming webhook is an event. To check that, we need to look at the record_type using the following check: ```python def respond(): //Check record_type of object data = request.json['data'] if data.get('record_type') == 'event': print(request.json[‘data’]) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added Then, we can check and see what kind of event it is. In the case of the example json above, the event is call.initiated. We can get that value using the following added code: ```python def respond(): //Check record_type of object data = request.json['data'] if data.get('record_type') == 'event': //Check event type event = data.get('event_type') print(event, flush=True) if event == "call.initiated": print("Incoming call", flush=True) print(request.json[‘data’]) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added As you can see, this check will print out “incoming call” whenever a call.initiated event is received by our application. We can even test it by giving the Phone Number associated with our Voice API Application a call! Now we can start to implement some commands in response to this webhook. ### Receiving Webhooks & creating a conference Below is the logic that will go inside our respond() function. When we receive a webhook, we extract the data from `request.json.get('data')` and we look at the `event_type` inside that object to determine a course of action. ```python calls = [] conference = None class call_info: call_control_id: '' call_leg_id: '' @app.route('/webhook', methods=['POST']) def respond(): # Activate global calls array global calls global conference # Get the data from the request data = request.json.get('data') # Check record_type if data.get('record_type') == 'event': # Check event type event = data.get('event_type') if event == "call.initiated": # Extract call information and store it in a new call_info() object new_call = call_info() new_call.call_control_id = data.get('payload').get('call_control_id') new_call.call_leg_id = data.get('payload').get('call_leg_id') calls.append(new_call) # Answer the call print(telnyx.Call.answer(new_call), flush=True) # When the call is answered, find the stored call and either add it # to the conference or create a new one if one is not yet created elif event == "call.answered": call_id = data.get('payload').get('call_control_id') call_created = call_info() call_created.call_control_id = call_id for call in calls: if call.call_control_id == call_id: if not conference: conference = telnyx.Conference.create(beep_enabled="always",call_control_id=call_id, name="demo-conference") else: conference.join(call_control_id=call_id) # When a caller hangs up, remove that caller from the global calls array elif event == "call.hangup": call_id = data.get('payload').get('call_leg_id') for call in calls: if call.call_leg_id == call_id: calls.remove(call) return Response(status=200) ``` After pasting the above content, Kindly check and remove any new line added Pat youself on the back - that's a lot of code to go through! Now let's break it down even further and explain what it does. First, create an array for keeping track of the ongoing calls and define a variable for storing the conference object. Then, we create a small object class for call_info, containing the call_control_id and call_leg_id. This will be useful for searching for calls in our calls array later, as well as using Conference Commands with those objects. ```python calls = [] conference = None class call_info: call_control_id: '' call_leg_id: '' ``` After pasting the above content, Kindly check and remove any new line added Next, we parse the data from our webhook in the respond() function. We first declare our global variables inside of the function so that the scope is consistent. Then, we extract the data from the reponse and check to ensure the `record_type` is `event`. Then, we extract the `event_type` itself and use logic to determine the action taken based on the `event`. ```python @app.route('/webhook', methods=['POST']) def respond(): # Activate global calls array global calls global conference # Get the data from the request data = request.json.get('data') #print(data, flush=True) #For testing purposes, you could print out the data object received # Check record_type if data.get('record_type') == 'event': # Check event type event = data.get('event_type') ``` After pasting the above content, Kindly check and remove any new line added Here is where you will respond to a new call being initiated, which can be from either an inbound or outbound call. We create a new call_info() object and assign the `call_control_id` and `call_leg_id` from the incoming data. We then use `telnyx.Call.answer(new_call)` to answer the call. This will trigger a webhook event `call.answered` which we will handle below. ```python # When call is initiated, create the new call object and add it to the calls array if event == "call.initiated": # Extract call information and store it in a new call_info() object new_call = call_info() new_call.call_control_id = data.get('payload').get('call_control_id') new_call.call_leg_id = data.get('payload').get('call_leg_id') calls.append(new_call) # Answer the call print(telnyx.Call.answer(new_call), flush=True) ``` After pasting the above content, Kindly check and remove any new line added On the `call.answered` event, retrieve the stored call created during the `call.initiated` event. Then, either create a new conference if this is the first call and there isn't a conference running yet, or add the call to an existing conference. Note that a `call_control_id` is required to start a conference, so there must aready be an existing call before you can create a conference, which is why we create the conference here. ```ruby # When the call is answered, find the stored call and either add it # to the conference or create a new one if one is not yet created elif event == "call.answered": call_id = data.get('payload').get('call_control_id') call_created = call_info() call_created.call_control_id = call_id for call in calls: if call.call_control_id == call_id: if not conference: conference = telnyx.Conference.create(beep_enabled="always",call_control_id=call_id, name="demo-conference") else: conference.join(call_control_id=call_id) ``` After pasting the above content, Kindly check and remove any new line added And finally, when a call ends we remove it from the active call list. ```python # When a caller hangs up, remove that caller from the global calls array elif event == "call.hangup": call_id = data.get('payload').get('call_leg_id') for call in calls: if call.call_leg_id == call_id: calls.remove(call) ``` After pasting the above content, Kindly check and remove any new line added ### Conclusion The full tutorial with comments can be found on Github. ## PHP ⏱ **60 minutes build time || Github Repo** ### Introduction The [Voice API framework](/api-reference/call-commands/dial), previously called Call Control, is a set of APIs that allow complete control of a call flow from the moment a call begins to the moment it is completed. In between, you will receive a number of [webhooks](/docs/voice/programmable-voice/receiving-webhooks) for each step of the call, allowing you to act on these events and send commands using the Telnyx Library. A subset of the operations available in the Telnyx Voice API is the [Conference](/api-reference/conference-commands/conference-recording-start) API. This allows the user (you) to create and manage a conference programmatically upon receiving an incoming call, or when initiating an outgoing call. The Telnyx PHP Library is a convenient wrapper around the Telnyx REST API. It allows you to access and control call flows using an intuitive object-oriented library. This tutorial will walk you through creating a simple Slim server that allows you to create and manage a conference. ### What can you do At the end of this tutorial you'll have an application that: * Verifies inbound webhooks are indeed from Telnyx * Creates a conference for the first caller * Adds additional callers to the existing conference * Tears down the conference when the last call leaves * Will create a new conference when the next caller dials in ### Setup Before beginning, please setup ensure that you have composer installed. #### Install packages ```bash composer require slim/slim:^4.0 composer require slim/http composer require slim/psr7 composer require telnyx/telnyx-php composer require vlucas/phpdotenv ``` After pasting the above content, Kindly check and remove any new line added This will create `composer.json` file with the packages needed to run the application. This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. The Voice API Application needs to be setup to work with the conference control api: * make sure the *Webhook API Version* is **API v2** * Fill in the *Webhook URL* with the address the server will be running on. Alternatively, you can use a service like [ngrok](/development/development-tools/ngrok-setup/index#ngrok) to temporarily forward a local port to the internet to a random address and use that. We'll talk about this in more detail later. Finally, you need to create an API Key - make sure you save the key somewhere safe. #### Setting environment variables This tutorial uses the excellent phpenv package to manage environment variables. Create a `.env` file in your root directory to contain your API & Public key. **BE CAREFUL TO NOT SHARE YOUR KEYS WITH ANYONE** Recommended to add `.env` to your `.gitignore` file. Your `.env` file should look something like: ``` TELNYX_API_KEY="KEYABC123_ZXY321" TELNYX_PUBLIC_KEY="+lorem/ipsum/lorem/ipsum=" ``` After pasting the above content, Kindly check and remove any new line added ### Code-along Now create a folder `public` and a file in the public folder`index.php`, then write the following to setup the telnyx library. ```bash mkdir public touch public/index.php ``` After pasting the above content, Kindly check and remove any new line added #### Setup slim server and instantiate Telnyx ```php load(); $TELNYX_API_KEY = $_ENV['TELNYX_API_KEY']; $TELNYX_PUBLIC_KEY = $_ENV['TELNYX_PUBLIC_KEY']; $CONFERENCE_FILE_NAME = '../conference_id.txt'; Telnyx\Telnyx::setApiKey($TELNYX_API_KEY); Telnyx\Telnyx::setPublicKey($TELNYX_PUBLIC_KEY); // Instantiate Slim App $app = AppFactory::create(); // Add error middleware $app->addErrorMiddleware(true, true, true); ``` After pasting the above content, Kindly check and remove any new line added 📝 *Note* the `$CONFERENCE_FILE_NAME = '../conference_id.txt';` will be used to track conference state. ### Receiving Webhooks & creating a conference Now that you have setup your auth token, phone number, and connection, you can begin to use the API Library to make and control conferences. First, you will need to setup a Slim endpoint to receive webhooks for call and conference events. There are a number of webhooks that you should anticipate receiving during the lifecycle of each call and conference. This will allow you to take action in response to any number of events triggered during a call. In this example, you will use the `call.initiated`, `call.answered`, and `conference.ended` events to add calls to a conference and tear it down. Because you will need to wait until there is a running call before you can create a conference, plan to use call events to create the conference after a call is initiated. #### Basic routing & functions The basic overview of the application is as follows: 1. Verify webhook & create TelnyxEvent 2. Check event-type and route to the event handler 3. `call.initiated` events are answered 4. `call.answered` events check if there is a conference, if so; join, if not, create new conference 5. `conference.ended` will tear down the existing conference making way for a new one. #### Webhook validation middleware Telnyx signs each webhook that can be validated by checking the signature with your public key. This example adds the verification step as middleware to be included on all Telnyx endpoints. ```php //Callback signature verification $telnyxWebhookVerify = function (Request $request, RequestHandler $handler) { //Extract the raw contents $payload = $request->getBody()->getContents(); //Grab the signature $sigHeader = $request->getHeader('HTTP_TELNYX_SIGNATURE_ED25519')[0]; //Grab the timestamp $timeStampHeader = $request->getHeader('HTTP_TELNYX_TIMESTAMP')[0]; //Construct the Telnyx event which will validate the signature and timestamp $telnyxEvent = \Telnyx\Webhook::constructEvent($payload, $sigHeader, $timeStampHeader); //Add the event object to the request to keep context for future middleware $request = $request->withAttribute('telnyxEvent', $telnyxEvent); //Send to next middleware $response = $handler->handle($request); //return response back to Telnyx return $response; }; ``` After pasting the above content, Kindly check and remove any new line added ℹ️ For more details on middleware see Slim's documentation on Route Middleware #### Conference management For each call, we need to check if there is already a conference. In a more sophisticated application this would typically be solved by a connection to any given data store. For this demo, we're managing the state in a file on disc `$CONFERENCE_FILE_NAME`. ```php // Read the ID out of the file, if doesn't exist return FALSE function readConferenceFile (String $CONFERENCE_FILE_NAME) { if (!file_exists($CONFERENCE_FILE_NAME)) { return FALSE; } else { $conferenceFile = fopen($CONFERENCE_FILE_NAME, 'r') or die("Unable to open file!"); $fileConferenceId = fread($conferenceFile, filesize($CONFERENCE_FILE_NAME)); return $fileConferenceId; } } // Create the conference Id file and write the ID to disc function createConferenceFile (String $conferenceId, String $CONFERENCE_FILE_NAME) { $conferenceFile = fopen($CONFERENCE_FILE_NAME, 'w') or die ('Unable to open conference file'); fwrite($conferenceFile, $conferenceId); fclose($conferenceFile); return $conferenceId; }; // Delete the file; making way for a new conference to be created for next caller function deleteConferenceFile (String $CONFERENCE_FILE_NAME){ if (!file_exists($CONFERENCE_FILE_NAME)) { return; } if (!unlink($CONFERENCE_FILE_NAME)) { die ('Can not delete conference file'); } return; }; ``` After pasting the above content, Kindly check and remove any new line added #### Event Handlers and switch For each event (besides `call.initiated` we need to check the current state of the conference before making next steps) ```php //Adds the given call to the conference function addCallToConference (String $callControlId, String $conferenceId) { $conference = new Telnyx\Conference($conferenceId); $joinConferenceParameters = array( 'call_control_id' => $callControlId ); $conference->join($joinConferenceParameters); }; // creates a conference and creates the conference state file function createConference (String $callControlId, String $CONFERENCE_FILE_NAME) { $conferenceName = uniqid('conf-'); $conferenceParameters = array( 'call_control_id' => $callControlId, 'name' => $conferenceName, 'beep_enabled' => 'always' ); $newConference = Telnyx\Conference::create($conferenceParameters); $conferenceId = $newConference->id; createConferenceFile($conferenceId, $CONFERENCE_FILE_NAME); return $conferenceId; } // Speaks to our caller then determines whether to create a new conference or add to existing function handleAnswer (String $callControlId, String $CONFERENCE_FILE_NAME) { $speakParams = array( 'payload' => 'joining conference', 'voice' => 'female', 'language' => 'en-GB' ); $call = new Telnyx\Call($callControlId); $call->speak($speakParams); $existingConferenceId = readConferenceFile($CONFERENCE_FILE_NAME); if (!$existingConferenceId) { createConference($callControlId, $CONFERENCE_FILE_NAME); } else { addCallToConference($callControlId, $existingConferenceId); } return; }; // Add route $app->post('/Callbacks/Voice/Inbound', function (Request $request, Response $response) { global $CONFERENCE_FILE_NAME; // Get the parsed event from the request $telnyxEvent = $request->getAttribute('telnyxEvent'); // Extract the relevant information $data = $telnyxEvent->data; // Only _really_ care about events right now if ($data['record_type'] != 'event') { return $response->withStatus(200); } $callControlId = $data->payload['call_control_id']; $event = $data['event_type']; switch ($event) { case 'call.initiated': // Create a new call object $call = new Telnyx\Call($callControlId); // Then answer it $call->answer(); break; case 'call.answered': handleAnswer($callControlId, $CONFERENCE_FILE_NAME); break; case 'conference.ended': deleteConferenceFile($CONFERENCE_FILE_NAME); default: # other events less importante right now break; } // Let's play nice and return 200 return $response->withStatus(200); })->add($telnyxWebhookVerify); // run the thing! $app->run(); ``` After pasting the above content, Kindly check and remove any new line added ### Usage Start the server `php -S localhost:8000 -t public` When you are able to run the server locally, the final step involves making your application accessible from the internet. So far, we've set up a local web server. This is typically not accessible from the public internet, making testing inbound requests to web applications difficult. The best workaround is a tunneling service. They come with client software that runs on your computer and opens an outgoing permanent connection to a publicly available server in a data center. Then, they assign a public URL (typically on a random or custom subdomain) on that server to your account. The public server acts as a proxy that accepts incoming connections to your URL, forwards (tunnels) them through the already established connection and sends them to the local web server as if they originated from the same machine. The most popular tunneling tool is `ngrok`. Check out the [ngrok setup](/development/development-tools/ngrok-setup/index#ngrok) walkthrough to set it up on your computer and start receiving webhooks from inbound messages to your newly created application. Once you've set up `ngrok` or another tunneling service you can add the public proxy URL to your Connection in the Mission Control Portal. To do this, click the edit symbol \[✎] next to your Connection. In the "Webhook URL" field, paste the forwarding address from ngrok into the Webhook URL field. Add `/Callbacks/Voice/Inbound` to the end of the URL to direct the request to the webhook endpoint in your slim-php server. For now you'll leave “Failover URL” blank, but if you'd like to have Telnyx resend the webhook in the case where sending to the Webhook URL fails, you can specify an alternate address in this field. ### Complete Running Voice API Conference Application The Github Repo contains an extended version of the tutorial code above ready to run. ## Node ⏱ **60 minutes build time || Github Repo** Telnyx Conference System demo built on Voice API V2 and node.js. In this tutorial, you’ll learn how to: 1. Set up your development environment to use Telnyx Voice API using Node. 2. Build a simple Telnyx Voice API Conference System using Node. - - - * [Prerequisites](#prerequisites) * [Telnyx Voice API Basics](#get-started-with-telnyx-call-control) * [Understanding the Command Syntax](#understanding-the-command-syntax) * [Telnyx Voice API Basic Set](#telnyx-call-control-basic-set) * [Telnyx Voice API Conference Commands](#telnyx-call-control-conference-commands) * [Building a Conference System](#building-a-conference-system) * [Interacting with the Conference Room](#interacting-with-the-conference-room) * [Lightning-Up the Application](#lightning-up-the-application) - - - ### Prerequisites This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. You’ll also need to have `node` installed to continue. You can check this by running the following: ```bash $ node -v ``` After pasting the above content, Kindly check and remove any new line added If Node isn’t installed, follow the official installation instructions for your operating system to install it. You’ll need to have the following Node dependencies installed for the Telnyx Voice API: ```javascript import express from 'express'; import superagent from 'superagent'; import fs from 'fs'; ``` After pasting the above content, Kindly check and remove any new line added ### Get started with Telnyx Voice API For the Voice API application you’ll need to get a set of basic functions to perform Telnyx Voice API Commands plus Telnyx Voice API Conference specifics. This tutorial will be using the following subset of basic Telnyx Voice API Commands: * [Voice API Answer](/api-reference/call-commands/answer-call) * [Voice API Hangup](https://developers.telnyx.com/docs/voice/programmable-voice/texml-verbs/hangup/index#hangup) * [Voice API Speak](/api-reference/call-commands/speak-text) * [Voice API Dial](/api-reference/call-commands/dial) Plus all the Telnyx Voice API Conference Commands: * [Voice API Join Conference](/api-reference/conference-commands/join-a-conference) * [Voice API Mute Conference Participant](/api-reference/conference-commands/mute-conference-participants) * [Voice API Unmute Conference Participant](/api-reference/conference-commands/unmute-conference-participants) * [Voice API Hold Conference Participant](/api-reference/conference-commands/hold-conference-participants) * [Voice API Unhold Conference Participant](/api-reference/conference-commands/unhold-conference-participants) You can get the full set of available Telnyx Voice API Commands [here](/api-reference/call-commands/dial). For each Telnyx Voice API Command we will be creating a function that will execute an `HTTP POST` Request to back to Telnyx server. To execute this API we are using `superagent`, so make sure you have it installed. If not you can install it with the following command: ```bash $ npm install superagent --save ``` After pasting the above content, Kindly check and remove any new line added After that you’ll be able to use ‘superagent’ as part of your app code as follows: ```javascript import superagent from 'superagent'; ``` After pasting the above content, Kindly check and remove any new line added To make use of the Telnyx Voice API Command API you’ll need to set a Telnyx API Key. To check that go to Mission Control Portal and under the `Auth` tab you select `Auth V2`. There you'll find credentials for `Auth v2 API Keys`. Click on `Create API Key` and save the key that is shown to you. Please store it as you wont be able to fetch it later. Once you have it, you can include it on the telnyx-account-v2.json file. ```javascript "telnyx_api_auth_v2": "" ``` After pasting the above content, Kindly check and remove any new line added This application will also make use of a hosted audio file for the waiting tone while on [hold](/api-reference/conference-commands/hold-conference-participants): ```javascript "telnyx_waiting_url": "" ``` After pasting the above content, Kindly check and remove any new line added As well as the ID of the Voice API Application for the [Dial](/api-reference/call-commands/dial) command: ```javascript "telnyx_connection_id": "" ``` After pasting the above content, Kindly check and remove any new line added You can find the Voice API ID in the Mission Portal by editing the Voice API Application being used: ![Finding the Voice API ID for a Voice API Application](https://images.ctfassets.net/4b49ta6b3nwj/3QSrrdNoH5ar5hvnu3H2fY/fe52cb157da846063c26501647f76441/call-control-id.png) Once all dependencies are set, we can create a function for each Telnyx Voice API Command. All Commands will follow the same syntax: ```javascript function call_control_COMMAND_NAME(f_call_control_id, f_INPUT1, ...){ const cc_action = ‘COMMAND_NAME’; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${cc_action}`) .send({ PARAM1: f_INPUT1 }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added ### Understanding the Command Syntax There are several aspects of this function that deserve some attention: * `Function Input Parameters`: to execute every Telnyx Voice API Command you’ll need to feed your function with the following: * the `Call Control ID` * the input parameters, specific to the body of the Command you’re executing. Having these set as function input parameters will make it generic enough to reuse for different use cases: ```javascript function call_control_COMMAND_NAME(f_call_control_id, f_INPUT) ``` After pasting the above content, Kindly check and remove any new line added All Telnyx Voice API Commands will be expecting the `Call Control ID` except `Dial`. There you’ll get a new one for the leg generated as response. * `Name of the Call Control Command`: as detailed [here](/api-reference/call-commands/dial), the Command name is part of the API URL. In our code we call that the `action` name, and will feed the POST Request URL later: ```javascript const cc_action = ‘COMMAND_NAME’ ``` After pasting the above content, Kindly check and remove any new line added * `Building the Telnyx Call Control Command`: once you have the Command name defined, you should have all the necessary info to build the complete Telnyx Voice API Command: ```javascript const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${cc_action}`) .send({ PARAM1: f_INPUT1 }); ``` After pasting the above content, Kindly check and remove any new line added ``` In this example, you can see that the `Call Control ID` and the Action name will feed the URL of the API. The Telnyx API Key is passed to the Authentication header, and the body is formed with all of the different input parameters received for that specific Command. ``` * `Calling the Telnyx Call Control Command`: Having the request `headers` and `options`/`body` set, the only thing left is to execute the `POST Request` to run the command. For that we are making use of node's `request` module: ```javascript request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); ``` After pasting the above content, Kindly check and remove any new line added ### Telnyx Voice API basic set This is how every Telnyx Voice API Command used in this application looks: #### Voice API answer ```javascript function call_control_answer_call(f_telnyx_api_auth_v2, f_call_control_id, f_client_state_s) { const l_cc_action = 'answer'; const l_client_state_64 = null; if (f_client_state_s) { l_client_state_64 = Buffer.from(f_client_state_s).toString('base64'); } const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${l_cc_action}`) .send({ client_state: l_client_state_64 }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Voice API hangup ```javascript function call_control_hangup(f_telnyx_api_auth_v2, f_call_control_id) { const l_cc_action = 'hangup'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${l_cc_action}`) .send({}); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Voice API dial ```javascript function call_control_dial(f_telnyx_api_auth_v2, f_dest, f_from, f_connection_id) { const l_cc_action = 'dial'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${l_cc_action}`) .send({ to: f_dest, from: f_from, connection_id: f_connection_id, }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Voice API speak ```javascript function call_control_speak(f_telnyx_api_auth_v2, f_call_control_id, f_tts_text) { const l_cc_action = 'speak'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${l_cc_action}`) .send({ payload: f_tts_text, voice: g_ivr_voice, language: g_ivr_language, }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Voice API recording start ```javascript function call_control_record_start(f_telnyx_api_auth_v2, f_call_control_id) { const l_cc_action = 'record_start'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${l_cc_action}`) .send({ format: 'mp3', channels: 'dual' }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Voice API recording stop ```javascript function call_control_record_stop(f_telnyx_api_auth_v2, f_call_control_id) { const cc_action = 'record_stop'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${cc_action}`) .send({}); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added ### Telnyx Voice API Conference Commands This is what every Telnyx Voice API Conference Commands look like: #### Conference: create conference ```javascript function call_control_create_conf(f_telnyx_api_auth_v2, f_call_control_id, f_client_state_s, f_name, f_callback) { const cc_action = 'create_conf'; const l_client_state_64 = null; if (f_client_state_s) { l_client_state_64 = Buffer.from(f_client_state_s).toString('base64'); } const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${cc_action}`) .send({ call_control_id: f_call_control_id, name: f_name, client_state: l_client_state_64 }); request .then((response) => { const body = response.body; if (body.data) f_callback(null, body.data.id); }) .catch((error) => { console.log(error); f_callback(err); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Conference: Join conference ```javascript function call_control_join_conf(f_telnyx_api_auth_v2, f_call_control_id, f_conf_id, f_client_state_s) { const cc_action = 'join'; const l_client_state_64 = null; if (f_client_state_s) { l_client_state_64 = Buffer.from(f_client_state_s).toString('base64'); } const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/calls/${f_call_control_id}/actions/${cc_action}`) .send({ call_control_id: f_call_control_id, client_state: l_client_state_64 }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Conference: Mute participant ```javascript function call_control_mute(f_telnyx_api_auth_v2, f_conf_id, f_call_control_ids) { const cc_action = 'mute'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/conferences/${f_conf_id}/actions/${cc_action}`) .send({ call_control_ids: f_call_control_ids, }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Conference: Unmute participant ```javascript function call_control_unmute(f_telnyx_api_auth_v2, f_conf_id, f_call_control_ids) { const cc_action = 'unmute'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/conferences/${f_conf_id}/actions/${cc_action}`) .send({ call_control_ids: f_call_control_ids, }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Conference: Hold participant ```javascript function call_control_hold(f_telnyx_api_auth_v2, f_conf_id, f_call_control_ids, f_audio_url) { const cc_action = 'hold'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/conferences/${f_conf_id}/actions/${cc_action}`) .send({ call_control_ids: f_call_control_ids, audio_url: f_audio_url }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added #### Conference: Unhold participant ```javascript function call_control_unhold(f_telnyx_api_auth_v2, f_conf_id, f_call_control_ids, f_audio_url) { const cc_action = 'unhold'; const request = superagent .set('Content-Type', 'application/json') .set('Accept', 'application/json') .set('Authorization', `Bearer ${f_telnyx_api_auth_v2}`) .post(`https://api.telnyx.com/v2/conferences/${f_conf_id}/actions/${cc_action}`) .send({ call_control_ids: f_call_control_ids, audio_url: f_audio_url }); request .then((response) => { const body = response.body; }) .catch((error) => { console.log(error); }); } ``` After pasting the above content, Kindly check and remove any new line added `Client State`: within some of the Telnyx Voice API Commands list we presented, you probably noticed we were including the `Client State` parameter. `Client State` is the key to ensure that we can have several levels on our IVR while consuming the same Voice API Events. Because Voice API is stateless and async, your application will be receiving several events of the same type, e.g. user just included `DTMF`. With `Client State` you enforce a unique ID to be sent back to Telnyx which can be used within a particular Command flow, identifying it as being at Level 2 of a certain IVR for example. ### Building a conference system With all the basic and conference related Telnyx Voice API Commands set, we are ready to put them in the order that will create a simple Conference System. For that all we are going to do is to: 1. handle incoming calls and place participants in the conference 2. push for outgoing calls and place participants in the conference 3. maintain a participant list 4. greet the new participants before place them on the conference room 5. put the first participant automatically on hold 6. put a participant on-hold every-time he's the only participant on the conference room 7. un-hold the unique participant on the conference room when the second arrives 8. allow remote commands to list participants, force hold/unhold, force mute/unmute, force participant push To exemplify this process we created a simple API call that will be exposed as the webhook in Mission Portal. For that we would be using `express`: ```bash $ npm install request --save ``` After pasting the above content, Kindly check and remove any new line added With `express` we can create an API wrapper that uses `HTTP POST` to call our Request Token method: ```javascript rest.post('/'+g_appName+'/start', function (req, res) { // APP CODE GOES HERE }) ``` After pasting the above content, Kindly check and remove any new line added This would expose a webhook like the following: ``` https://:8081/telnyx-conf/start ``` You probably noticed that `g_appName` in the previous point. That is part of a set of global variables we are defining with a certain set of info we know we are going to use in this app: TTS parameters, like voice and language to be used, etc... For the purpose of maintaining the Conference list and state of the Conference room we also define a set of global variables. You can set these at the beginning of your code: ```javascript // Application: const g_appName = "telnyx-conf-v2"; // TTS Options const g_ivr_voice = 'female'; const g_ivr_language = 'en-GB'; // Conf Options const g_conf_id = 'no-conf'; const g_on_hold = 'false'; const g_participants = new Map(); ``` After pasting the above content, Kindly check and remove any new line added > > If you would like to run the application on your local machine you will have to expose the app to the public internet. To do this you can use `ngrok`. You can follow the setup guide for `ngrok` [here](/development/development-tools/ngrok-setup/index#ngrok). With that set, we can fill in that space that we named as `APP CODE GOES HERE`. When your webhook URL is ready you can add the webhook URL to your Mission Control Portal Connection associated with your number. Here's an example of what a Voice API setup looks like: ![Mission Control Portal Voice API setup](/img/cc-example-connection.png) So the first thing to be done is to identify the kind of event you just received and extract the `Call Control Id` and `Client State`: ```javascript if (req && req.body && req.body.data.event_type) { const l_hook_event_type = req.body.data.event_type; const l_call_control_id = req.body.data.payload.call_control_id; const l_client_state_64 = req.body.data.payload.client_state; } else{res.end('0');} ``` After pasting the above content, Kindly check and remove any new line added Once you identify the `Event Type` received, it’s just a matter of having your application reacting to that. Is the way you react to that Event that helps you creating the IVR logic. What you would be doing is to execute Telnyx Voice API Command as a reaction to those Events. > For consistency, the Telnyx Voice API engine requires every single Webhook to be replied to by the Webhook end-point, otherwise we will keep trying to send it. For that reason, we have to be ready to consume every Webhook we expect to receive and reply with 200 OK. #### `Webhook call initiated >> Command answer call` ```javascript if (req.body.data.payload.direction == 'incoming') call_control_answer_call(g_telnyx_api_auth_v2, l_call_control_id, null); else call_control_answer_call(g_telnyx_api_auth_v2, l_call_control_id, 'outgoing'); res.end(); ``` After pasting the above content, Kindly check and remove any new line added #### `Webhook call answered >> Start conference` Once your app is notified by Telnyx that the call was established you want to either start the conference room or put the participant in an already existing room. ```javascript if (g_conf_id == 'no-conf') { // First participant message call_control_speak(g_telnyx_api_auth_v2, l_call_control_id, 'Welcome to this conference demo. ' + 'Please wait for other participants to join. ' ); // Create Conference call_control_create_conf(g_telnyx_api_auth_v2, l_call_control_id, 'conf-created', 'myconf', function (conf_err, conf_res) { if (conf_res == '0') { console.log("[%s] LOG - Conference Creation Failed!", get_timestamp()); call_control_hangup(g_telnyx_api_auth_v2, l_call_control_id); } else { g_conf_id = conf_res; if (!l_client_state_64) g_participants.set(l_call_control_id, l_hook_from); // add inbound participant to the list else g_participants.set(l_call_control_id, l_hook_to); // add outbound participant to the list } }); } else { // Consequent participants message call_control_speak(g_telnyx_api_auth_v2, l_call_control_id, 'Welcome to this conference demo. ' + 'We are now putting you on the conference room. ' ); call_control_join_conf(g_telnyx_api_auth_v2, l_call_control_id, g_conf_id, 'agent-in'); // Add Participant to the Participant List if (!l_client_state_64) g_participants.set(l_call_control_id, l_hook_from); // add inbound participant to the list else g_participants.set(l_call_control_id, l_hook_to); // add outbound participant to the list } res.end(); ``` After pasting the above content, Kindly check and remove any new line added #### `Conference created >> Just log` Your app will be informed that the Conference was created. ```javascript console.log("[%s] LOG - New Conference Created! - Conference ID [%s]", get_timestamp(), g_conf_id); res.end(); } ``` After pasting the above content, Kindly check and remove any new line added #### `Conference join >> Hold/Unhold participant` Your app will be informed that a participant just joined the room. ```javascript if (g_participants.size < 2) { // First Participant call_control_hold(g_telnyx_api_auth_v2, g_conf_id, [l_call_control_id], g_telnyx_waiting_url); g_on_hold = l_call_control_id; } else if (g_participants.size == 2) { // Second Participant call_control_unhold(g_telnyx_api_auth_v2, g_conf_id, [g_on_hold]); g_on_hold = 'false'; } res.end(); ``` After pasting the above content, Kindly check and remove any new line added #### `Conference Leave >> Remove Participant / Cleanup Vars` Your app will be informed that a participant just left the room, we need to cleanup some things. ```javascript // Remove participant from the list g_participants.delete(l_call_control_id); // Reset Conf_Id if conference room empty if (g_participants.size < 1) { g_conf_id = 'no-conf'; // Put participant back on hold if it's the last one } else if (g_participants.size == 1) { for (let key of g_participants.keys()) { // First Participant call_control_hold(g_telnyx_api_auth_v2, g_conf_id, [key], g_telnyx_waiting_url); g_on_hold = key; } } res.end(); ``` After pasting the above content, Kindly check and remove any new line added #### `Anything Else >> Just Ack/200ok` ```javascript } else if (l_hook_event_type == 'call.speak.ended' || l_hook_event_type == 'call.speak.started' || l_hook_event_type == 'playback.ended' || l_hook_event_type == 'call.hangup' || l_hook_event_type == 'gather.ended' || l_hook_event_type == 'call.bridged' || l_hook_event_type == 'dtmf' || l_hook_event_type == 'playback.started') { res.end(); } ``` After pasting the above content, Kindly check and remove any new line added ### Interacting with the conference room As part of the process of building a Conference Room, there is also the possibility of interacting with the application to list participants and engage with direct participants. We do that by creating a couple of `HTTP GET` commands that can be then called by a browser, cURL or Postman. #### `Listing participants` *`https://:8081/telnyx-conf-v2/list`* ```javascript rest.get('/' + g_appName + '/list', function (req, res) { // Return/Display complete participant list if (g_participants.size > 0 && g_conf_id != 'no-conf') { let l_list = 'Conference ID: ' + g_conf_id + '\n'; l_list += '\n'; l_list += 'Participant List: \n'; for (let key of g_participants.keys()) { l_list += key + ' - [' + g_participants.get(key) + '] \n'; } res.end(l_list); } else res.end("no participant or no conference exists"); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Mute participant` *`https://:8081/telnyx-conf-v2/mute?participant=x`* ```javascript rest.get('/' + g_appName + '/mute', function (req, res) { // Mute specific Participant if (g_participants.size > 0 && g_conf_id != 'no-conf') { call_control_mute(g_telnyx_api_auth_v2, g_conf_id, [req.query.participant]); res.end("participant muted [" + req.query.participant + "]"); } else res.end("no participant or no conference exists"); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Unmute participant` *`https://:8081/telnyx-conf-v2/unmute?participant=x`* ```javascript rest.get('/' + g_appName + '/unmute', function (req, res) { // Un-Mute specific Participant if (g_participants.size > 0 && g_conf_id != 'no-conf') { call_control_unmute(g_telnyx_api_auth_v2, g_conf_id, [req.query.participant]); res.end("participant unmuted [" + req.query.participant + "]"); } else res.end("no participant or no conference exists"); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Hold participant` *`https://:8081/telnyx-conf-v2/hold?participant=x`* ```javascript rest.get('/' + g_appName + '/hold', function (req, res) { // Put specific participant on-hold if (g_participants.size > 0 && g_conf_id != 'no-conf') { call_control_hold(g_telnyx_api_key_v1, g_telnyx_api_secret_v1, g_conf_id, [req.query.participant], g_telnyx_waiting_url); res.end("participant on hold [" + req.query.participant + "]"); } else res.end("no participant or no conference exists"); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Unhold participant` *`https://:8081/telnyx-conf-v2/unhold?participant=x`* ```javascript rest.get('/' + g_appName + '/unhold', function (req, res) { // Un-hold specific participant if (g_participants.size > 0 && g_conf_id != 'no-conf') { call_control_unhold(g_telnyx_api_key_v1, g_telnyx_api_secret_v1, g_conf_id, [req.query.participant]); res.end("participant resumed [" + req.query.participant + "]"); } else res.end("no participant or no conference exists"); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Pull participant` *`https://:8081/telnyx-conf-v2/pull?number=x`* Please note that a URL encoded number format is expected by the webhook, so for international `+E164` numbers we should replace `+` per `%2B`. Example: *`https://:8081/telnyx-conf-v2/pull?number=%2B35193309090`* ```javascript rest.get('/' + g_appName + '/pull', function (req, res) { // Dial-out to specific number to pull participant call_control_dial(g_telnyx_api_auth_v2, req.query.number, "conf", g_telnyx_connection_id); res.end("called " + req.query.number); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Start recording call leg` *`https://:8081/telnyx-conf-v2/record-start?participant=x`* ```javascript rest.get('/' + g_appName + '/record-start', function (req, res) { call_control_record_start(g_telnyx_api_auth_v2, req.query.participant); res.end("recording started for " + req.query.participant); }) ``` After pasting the above content, Kindly check and remove any new line added #### `Stop recording call leg` *`https://:8081/telnyx-conf-v2/record-stop?participant=x`* ```javascript rest.get('/' + g_appName + '/record-stop', function (req, res) { call_control_record_stop(g_telnyx_api_auth_v2, req.query.participant); res.end("recording stopped for " + req.query.participant); }) ``` After pasting the above content, Kindly check and remove any new line added ### Lightning-up the application Finally the last piece of the puzzle is having your application listening for Telnyx Webhooks: ```javascript const server = rest.listen(8081, function () { const host = server.address().address const port = server.address().port }) ``` After pasting the above content, Kindly check and remove any new line added ## Ruby ⏱ **60 minutes build time || Github Repo** ### Introduction to conferencing The [Voice API framework](/api-reference/call-commands/dial), previously called Call Control, is a set of APIs that allow complete control of a call flow from the moment a call begins to the moment it is completed. In between, you will receive a number of [webhooks](/docs/voice/programmable-voice/receiving-webhooks) for each step of the call, allowing you to act on these events and send commands using the Telnyx Library. A subset of the operations available in the Voice API is the [ Conference](/api-reference/conference-commands/conference-recording-start) API. This allows the user (you) to create and manage a conference programmatically upon receiving an incoming call, or when initiating an outgoing call. The Telnyx Ruby Library is a convenient wrapper around the Telnyx REST API. It allows you to access and control call flows using an intuitive object-oriented library. This tutorial will walk you through creating a simple Sinatra server that allows you to create and manage a conference. ### Setting up your environment Before beginning, please ensure that you have the Telnyx and Sinatra gems installed. ```bash gem install telnyx sinatra ``` After pasting the above content, Kindly check and remove any new line added Alternatively, create a Gemfile for your project ```ruby source 'https://rubygems.org' gem 'sinatra' gem 'telnyx' ``` After pasting the above content, Kindly check and remove any new line added This tutorial assumes you've already [set up your developer account and environment](/development) and you know how to [send commands](/docs/voice/programmable-voice/sending-commands) and [receive webhooks](/docs/voice/programmable-voice/receiving-webhooks) using the Telnyx Voice API. The Voice API Application needs to be setup to work with the conference control api: * make sure the *Webhook API Version* is **API v2** * Fill in the *Webhook URL* with the address the server will be running on. Alternatively, you can use a service like [Ngrok](https://ngrok.com) to temporarily forward a local port to the internet to a random address and use that. We'll talk about this in more detail later. Finally, you need to create an API Key - make sure you save the key somewhere safe. Now create a file such as `conference_demo_server.rb`, then write the following to setup the telnyx library. ```ruby require 'sinatra' require 'telnyx' CONFIG = { # The following 3 keys need to be filled out telnyx_api_key: 'YOUR_API_KEY', phone_number: 'TELNYX_PHONE_NUMBER', # the number that will be used for accessing the conference connection_id: 'CONNECTION_ID', # the connection id for phone number above } # Setup telnyx api key. Telnyx.api_key = CONFIG[:telnyx_api_key] ``` After pasting the above content, Kindly check and remove any new line added ### Receiving webhooks & creating a conference Now that you have setup your auth token, phone number, and connection, you can begin to use the API Library to make and control conferences. First, you will need to setup a Sinatra endpoint to receive webhooks for call and conference events. There are a number of webhooks that you should anticipate receiving during the lifecycle of each call and conference. This will allow you to take action in response to any number of events triggered during a call. In this example, you will use the `call.initiated` and `call.answered` events to add call to a conference. Because you will need to wait until there is a running call before you can create a conference, plan to use call events to create the conference after a call is initiated. ```ruby # ... # Declare script level variables calls = [] conference = nil set :port, 9090 post "/webhook" do # Parse the request body. request.body.rewind data = JSON.parse(request.body.read)['data'] # Handle events if data['record_type'] == 'event' case data['event_type'] when 'call.initiated' # Create a new call object. call = Telnyx::Call.new id: data['payload']['call_control_id'], call_leg_id: data['payload']['call_leg_id'] # Save the new call object into our call list for later use. calls << call # Answer the call, this will cause the api to send another webhook event # of the type call.answered, which we will handle below. call.answer when 'call.answered' # Find the stored call, which was created during a call.initiated event. call = calls.find { |call| call.id == data['payload']['call_control_id'] } # Create a new conference if this is the first caller and there # is no conference running yet. if conference.nil? conference = Telnyx::Conferences.create call_control_id: call.id, name: 'demo-conference' # If there is a conference, then add the new caller. else conference.join call_control_id: call.id end when 'call.hangup' # Remove the ended call from the active call list calls.reject! {|call| call.call_leg_id == data['payload']['call_leg_id']} end end end ``` After pasting the above content, Kindly check and remove any new line added Pat yourself on the back - that's a lot of code to go through! Now let's break it down even further and explain what it does. First, create an array for keeping track of the ongoing calls and define a variable for storing the conference object. Then, tell Sinatra to listen on port 9090 and create an endpoint at `/webhook`, which can be anything you choose as the API doesn't care; here we just call it webhook. ```ruby calls = [] conference = nil set :port, 9090 post "/webhook" do # ... end ``` After pasting the above content, Kindly check and remove any new line added Next, parse the data from the API server, check to see if it is a webhook event, and act on it if it is. Then, you will define what actions to take on different types of events. ```ruby post "/webhook" do request.body.rewind data = JSON.parse(request.body.read)['data'] if data['record_type'] == 'event' case data['event_type'] # ... end end ``` After pasting the above content, Kindly check and remove any new line added Here is where you will respond to a new call being initiated, which can be from either an inbound or outbound call. Create a new `Telnyx::Call` object and store it in the active call list, then call `call.answer` to answer it if it's an inbound call. ```ruby when 'call.initiated' call = Telnyx::Call.new id: data['payload']['call_control_id'], call_leg_id: data['payload']['call_leg_id'] calls << call call.answer ``` After pasting the above content, Kindly check and remove any new line added On the `call.answered` event, retrieve the stored call created during the `call.initiated` event. Then, either create a new conference if this is the first call and there isn't a conference running yet, or add the call to an existing conference. Note that a `call_control_id` is required to start a conference, so there must aready be an existing call before you can create a conference, which is why we create the conference here. ```ruby when 'call.answered' call = calls.find { |call| call.id == data['payload']['call_control_id'] } if conference.nil? conference = Telnyx::Conferences.create call_control_id: call.id, name: 'demo-conference' else conference.join call_control_id: call.id end ``` After pasting the above content, Kindly check and remove any new line added And finally, when a call ends we remove it from the active call list. ```ruby when 'call.hangup' puts 'Call hung up' calls.reject! {|call| call.call_leg_id == data['payload']['call_leg_id']} ``` After pasting the above content, Kindly check and remove any new line added ### Authentication for your conferencing application Now you have a working conference application! How secure is it though? Could a 3rd party simply craft fake webhooks to manipulate the call flow logic of your application? Telnyx has you covered with a powerful signature verification system! Simply make the following changes: ```ruby # ... ENV['TELNYX_PUBLIC_KEY'] = 'YOUR_PUBLIC_KEY' # Please fetch the public key from: https://portal.telnyx.com/#/app/account/public-key post '/webhook' do request.body.rewind body = request.body.read # Save the body for verification later data = JSON.parse(body)['data'] Telnyx::Webhook::Signature.verify(body, request.env['HTTP_TELNYX_SIGNATURE_ED25519'], request.env['HTTP_TELNYX_TIMESTAMP']) # ... ``` After pasting the above content, Kindly check and remove any new line added Fill in the public key from the Telnyx Portal here. `Telnyx::Webhook::Signature.verify` will do the work of verifying the authenticity of the message, and raise `SignatureVerificationError` if the signature does not match the payload. ### Conferencing usage If you used a Gemfile, start the conference server with `bundle exec ruby conference_demo_server.rb`, if you are using globally installed gems use `ruby conference_demo_server.rb`. When you are able to run the server locally, the final step involves making your application accessible from the internet. So far, we've set up a local web server. This is typically not accessible from the public internet, making testing inbound requests to web applications difficult. The best workaround is a tunneling service. They come with client software that runs on your computer and opens an outgoing permanent connection to a publicly available server in a data center. Then, they assign a public URL (typically on a random or custom subdomain) on that server to your account. The public server acts as a proxy that accepts incoming connections to your URL, forwards (tunnels) them through the already established connection and sends them to the local web server as if they originated from the same machine. The most popular tunneling tool is `ngrok`. Check out the [ngrok setup](/development/development-tools/ngrok-setup/index#ngrok) walkthrough to set it up on your computer and start receiving webhooks from inbound messages to your newly created application. Once you've set up `ngrok` or another tunneling service you can add the public proxy URL to your Connection in the Mission Control Portal. To do this, click the edit symbol \[✎] next to your Connection. In the "Webhook URL" field, paste the forwarding address from ngrok into the Webhook URL field. Add `/webhooks` to the end of the URL to direct the request to the webhook endpoint in your Sinatra server. For now you'll leave “Failover URL” blank, but if you'd like to have Telnyx resend the webhook in the case where sending to the Webhook URL fails, you can specify an alternate address in this field. ### Complete running Voice API conference application The api-v2 directory contains an extended version of the tutorial code above, with the added ability to control the conference from the console! See the comments in the code for details on invoking the commands. --- ## Regional & Restrictions ### Voice API Services in Europe > Source: https://developers.telnyx.com/docs/voice/programmable-voice/voice-api-services-in-europe.md ## Overview Telnyx now has a dedicated endpoint - https://api.telnyx.eu, that can be used to help reduce the latency on calls held in Europe. *Don't forget to update `YOUR_API_KEY` here.* ```bash curl --location --request POST 'https://api.telnyx.com/v2/calls' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data-raw '{ "to":"+18727726004", "from":"+18022455739", "connection_id":"1684641123236054244" }' ``` After pasting the above content, Kindly check and remove any new line added To receive Voice API calls in Europe, Telnyx users should set the AnchorSite® for the application to one of the European Anchorsites—either Frankfurt, London or Amsterdam. Users can change the AnchorSite® on any given application by editing their existing application and scrolling to the AnchorSite® Selection filed, as below: ![Voice API services in Europe](/img/voice_programmable-voice_voice-api-services-in-europe_portal-voice-application-settings-anchorsite-selection.png) > Please note that all the participants of the conferences and the calls to add to the queue must be in the same region. --- ### L1 Account Restrictions > Source: https://developers.telnyx.com/docs/voice/programmable-voice/l1-accounts-restirctions.md The accounts with the L1 verification are restricted in the following way: - All machine-generated speak commands are pre-pended with "This is an automated call generated on the Telnyx platform, please report any abuse to fraud@telnyx.com". This currently includes: - /v2/calls - /v2/calls/:call_control_id/actions/transfer - /v2/calls/:call_control_id/actions/gather_using_audio - /v2/calls/:call_control_id/actions/gather_using_speak - /v2/calls/:call_control_id/actions/playback_start - /v2/calls/:call_control_id/actions/speak - /v2/calls/:call_control_id/actions/gather_using_ai - /v2/calls/:call_control_id/actions/ai_assistant_start - and the TeXML verbs: - Play - Say - AIGather - Limited to a maximum of 100 outbound calls a day. - Limited to 10 outbound calls per hour. --- ## API Reference (Voice API) ### Call Control Applications - [List call control applications](https://developers.telnyx.com/api-reference/call-control-applications/list-call-control-applications.md): Return a list of call control applications. - [Create a call control application](https://developers.telnyx.com/api-reference/call-control-applications/create-a-call-control-application.md): Create a call control application. - [Retrieve a call control application](https://developers.telnyx.com/api-reference/call-control-applications/retrieve-a-call-control-application.md): Retrieves the details of an existing call control application. - [Update a call control application](https://developers.telnyx.com/api-reference/call-control-applications/update-a-call-control-application.md): Updates settings of an existing call control application. - [Delete a call control application](https://developers.telnyx.com/api-reference/call-control-applications/delete-a-call-control-application.md): Deletes a call control application. ### Call Information - [List all active calls for given connection](https://developers.telnyx.com/api-reference/call-information/list-all-active-calls-for-given-connection.md): Lists all active calls for given connection. Acceptable connections are either SIP connections with webhook_url or xml_request_url, call control or texml. Retu… - [Retrieve a call status](https://developers.telnyx.com/api-reference/call-information/retrieve-a-call-status.md): Returns the status of a call (data is available 10 minutes after call ended). ### Debugging - [List call events](https://developers.telnyx.com/api-reference/debugging/list-call-events.md): Filters call events by given filter parameters. Events are ordered by `occurred_at`. If filter for `leg_id` or `application_session_id` is not present, it only… ### Call Commands - [Dial](https://developers.telnyx.com/api-reference/call-commands/dial.md): Dial a number or SIP URI from a given connection. A successful response will include a `call_leg_id` which can be used to correlate the command with subsequent… - [Answer call](https://developers.telnyx.com/api-reference/call-commands/answer-call.md): Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call. - [Bridge calls](https://developers.telnyx.com/api-reference/call-commands/bridge-calls.md): Bridge two call control calls. - [Start AI Assistant](https://developers.telnyx.com/api-reference/call-commands/start-ai-assistant.md): Start an AI assistant on the call. - [Join AI Assistant Conversation](https://developers.telnyx.com/api-reference/call-commands/join-ai-assistant-conversation.md): Add a participant to an existing AI assistant conversation. Use this command to bring an additional call leg into a running AI conversation. - [Stop AI Assistant](https://developers.telnyx.com/api-reference/call-commands/stop-ai-assistant.md): Stop an AI assistant on the call. - [Add messages to AI Assistant](https://developers.telnyx.com/api-reference/call-commands/add-messages-to-ai-assistant.md): Add messages to the conversation started by an AI assistant on the call. - [Start Conversation Relay](https://developers.telnyx.com/api-reference/call-commands/start-conversation-relay.md): Start a Conversation Relay session on an active call. Conversation Relay connects the call audio to your WebSocket so your application can exchange realtime me… - [Stop Conversation Relay](https://developers.telnyx.com/api-reference/call-commands/stop-conversation-relay.md): Stop the active Conversation Relay session on a call. - [Update client state](https://developers.telnyx.com/api-reference/call-commands/update-client-state.md): Updates client state - [Enqueue call](https://developers.telnyx.com/api-reference/call-commands/enqueue-call.md): Put the call in a queue. - [Forking start](https://developers.telnyx.com/api-reference/call-commands/forking-start.md): Call forking allows you to stream the media from a call to a specific target in realtime. - [Forking stop](https://developers.telnyx.com/api-reference/call-commands/forking-stop.md): Stop forking a call. - [Gather](https://developers.telnyx.com/api-reference/call-commands/gather.md): Gather DTMF signals to build interactive menus. - [Gather stop](https://developers.telnyx.com/api-reference/call-commands/gather-stop.md): Stop current gather. - [Gather using AI](https://developers.telnyx.com/api-reference/call-commands/gather-using-ai.md): Gather parameters defined in the request payload using a voice assistant. - [Gather using audio](https://developers.telnyx.com/api-reference/call-commands/gather-using-audio.md): Play an audio file on the call until the required DTMF signals are gathered to build interactive menus. - [Gather using speak](https://developers.telnyx.com/api-reference/call-commands/gather-using-speak.md): Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus. - [Hangup call](https://developers.telnyx.com/api-reference/call-commands/hangup-call.md): Hang up the call. - [Reject a call](https://developers.telnyx.com/api-reference/call-commands/reject-a-call.md): Reject an incoming call. - [Remove call from a queue](https://developers.telnyx.com/api-reference/call-commands/remove-call-from-a-queue.md): Removes the call from a queue. - [Play audio URL](https://developers.telnyx.com/api-reference/call-commands/play-audio-url.md): Play an audio file on the call. If multiple play audio commands are issued consecutively, - [Stop audio playback](https://developers.telnyx.com/api-reference/call-commands/stop-audio-playback.md): Stop audio being played on the call. - [Recording start](https://developers.telnyx.com/api-reference/call-commands/recording-start.md): Start recording the call. Recording will stop on call hang-up, or can be initiated via the Stop Recording command. - [Recording stop](https://developers.telnyx.com/api-reference/call-commands/recording-stop.md): Stop recording the call. - [Record resume](https://developers.telnyx.com/api-reference/call-commands/record-resume.md): Resume recording the call. - [Record pause](https://developers.telnyx.com/api-reference/call-commands/record-pause.md): Pause recording the call. Recording can be resumed via Resume recording command. - [Send DTMF](https://developers.telnyx.com/api-reference/call-commands/send-dtmf.md): Sends DTMF tones from this leg. DTMF tones will be heard by the other end of the call. - [SIP Refer a call](https://developers.telnyx.com/api-reference/call-commands/sip-refer-a-call.md): Initiate a SIP Refer on a Call Control call. You can initiate a SIP Refer at any point in the duration of a call. - [SIPREC start](https://developers.telnyx.com/api-reference/call-commands/siprec-start.md): Start siprec session to configured in SIPREC connector SRS. - [SIPREC stop](https://developers.telnyx.com/api-reference/call-commands/siprec-stop.md): Stop SIPREC session. - [Speak text](https://developers.telnyx.com/api-reference/call-commands/speak-text.md): Convert text to speech and play it back on the call. If multiple speak text commands are issued consecutively, the audio files will be placed in a queue awaiti… - [Streaming start](https://developers.telnyx.com/api-reference/call-commands/streaming-start.md): Start streaming the media from a call to a specific WebSocket address or Dialogflow connection in near-realtime. Audio will be delivered as base64-encoded RTP… - [Streaming stop](https://developers.telnyx.com/api-reference/call-commands/streaming-stop.md): Stop streaming a call to a WebSocket. - [Noise Suppression Start (BETA)](https://developers.telnyx.com/api-reference/call-commands/noise-suppression-start-beta.md) - [Noise Suppression Stop (BETA)](https://developers.telnyx.com/api-reference/call-commands/noise-suppression-stop-beta.md) - [Switch supervisor role](https://developers.telnyx.com/api-reference/call-commands/switch-supervisor-role.md): Switch the supervisor role for a bridged call. This allows switching between different supervisor modes during an active call - [Transcription start](https://developers.telnyx.com/api-reference/call-commands/transcription-start.md): Start real-time transcription. Transcription will stop on call hang-up, or can be initiated via the Transcription stop command. - [Transcription stop](https://developers.telnyx.com/api-reference/call-commands/transcription-stop.md): Stop real-time transcription. - [Transfer call](https://developers.telnyx.com/api-reference/call-commands/transfer-call.md): Transfer a call to a new destination. If the transfer is unsuccessful, a `call.hangup` webhook for the other call (Leg B) will be sent indicating that the tran… ### Callbacks - [Call Initiated](https://developers.telnyx.com/api-reference/callbacks/call-initiated.md) - [Call Answered](https://developers.telnyx.com/api-reference/callbacks/call-answered.md) - [Call Streaming Started](https://developers.telnyx.com/api-reference/callbacks/call-streaming-started.md) - [Call Streaming Stopped](https://developers.telnyx.com/api-reference/callbacks/call-streaming-stopped.md) - [Call Streaming Failed](https://developers.telnyx.com/api-reference/callbacks/call-streaming-failed.md) - [Call Deepfake Detection Result](https://developers.telnyx.com/api-reference/callbacks/call-deepfake-detection-result.md) - [Call Deepfake Detection Error](https://developers.telnyx.com/api-reference/callbacks/call-deepfake-detection-error.md) - [Call Bridged](https://developers.telnyx.com/api-reference/callbacks/call-bridged.md) - [Call Conversation Ended](https://developers.telnyx.com/api-reference/callbacks/call-conversation-ended.md) - [Call Conversation Insights Generated](https://developers.telnyx.com/api-reference/callbacks/call-conversation-insights-generated.md) - [Call Enqueued](https://developers.telnyx.com/api-reference/callbacks/call-enqueued.md) - [Call Left Queue](https://developers.telnyx.com/api-reference/callbacks/call-left-queue.md) - [Call Fork Started](https://developers.telnyx.com/api-reference/callbacks/call-fork-started.md) - [Call Fork Stopped](https://developers.telnyx.com/api-reference/callbacks/call-fork-stopped.md) - [Call Gather Ended](https://developers.telnyx.com/api-reference/callbacks/call-gather-ended.md) - [Call Dtmf Received](https://developers.telnyx.com/api-reference/callbacks/call-dtmf-received.md) - [Call AI Gather Ended](https://developers.telnyx.com/api-reference/callbacks/call-ai-gather-ended.md) - [Call AI Gather Message History Updated](https://developers.telnyx.com/api-reference/callbacks/call-ai-gather-message-history-updated.md) - [Call AI Gather Partial Results](https://developers.telnyx.com/api-reference/callbacks/call-ai-gather-partial-results.md) - [Call Playback Started](https://developers.telnyx.com/api-reference/callbacks/call-playback-started.md) - [Call Playback Ended](https://developers.telnyx.com/api-reference/callbacks/call-playback-ended.md) - [Call Hangup](https://developers.telnyx.com/api-reference/callbacks/call-hangup.md) - [Call Recording Saved](https://developers.telnyx.com/api-reference/callbacks/call-recording-saved.md) - [Call Speak Ended](https://developers.telnyx.com/api-reference/callbacks/call-speak-ended.md) - [Call Recording Error](https://developers.telnyx.com/api-reference/callbacks/call-recording-error.md) - [Call Recording Transcription Saved](https://developers.telnyx.com/api-reference/callbacks/call-recording-transcription-saved.md) - [Call Refer Started](https://developers.telnyx.com/api-reference/callbacks/call-refer-started.md) - [Call Refer Completed](https://developers.telnyx.com/api-reference/callbacks/call-refer-completed.md) - [Call Refer Failed](https://developers.telnyx.com/api-reference/callbacks/call-refer-failed.md) - [Call Siprec Started](https://developers.telnyx.com/api-reference/callbacks/call-siprec-started.md) - [Call Siprec Stopped](https://developers.telnyx.com/api-reference/callbacks/call-siprec-stopped.md) - [Call Siprec Failed](https://developers.telnyx.com/api-reference/callbacks/call-siprec-failed.md) - [Call Speak Started](https://developers.telnyx.com/api-reference/callbacks/call-speak-started.md) - [Transcription](https://developers.telnyx.com/api-reference/callbacks/transcription.md) - [Call Machine Detection Ended](https://developers.telnyx.com/api-reference/callbacks/call-machine-detection-ended.md) - [Call Machine Greeting Ended](https://developers.telnyx.com/api-reference/callbacks/call-machine-greeting-ended.md) - [Call Machine Premium Detection Ended](https://developers.telnyx.com/api-reference/callbacks/call-machine-premium-detection-ended.md) - [Call Machine Premium Greeting Ended](https://developers.telnyx.com/api-reference/callbacks/call-machine-premium-greeting-ended.md) - [Conference Created](https://developers.telnyx.com/api-reference/callbacks/conference-created.md) - [Conference Ended](https://developers.telnyx.com/api-reference/callbacks/conference-ended.md) - [Conference Floor Changed](https://developers.telnyx.com/api-reference/callbacks/conference-floor-changed.md) - [Conference Participant Joined](https://developers.telnyx.com/api-reference/callbacks/conference-participant-joined.md) - [Conference Participant Left](https://developers.telnyx.com/api-reference/callbacks/conference-participant-left.md) - [Conference Participant Playback Ended](https://developers.telnyx.com/api-reference/callbacks/conference-participant-playback-ended.md) - [Conference Participant Playback Started](https://developers.telnyx.com/api-reference/callbacks/conference-participant-playback-started.md) - [Conference Participant Speak Ended](https://developers.telnyx.com/api-reference/callbacks/conference-participant-speak-ended.md) - [Conference Participant Speak Started](https://developers.telnyx.com/api-reference/callbacks/conference-participant-speak-started.md) - [Conference Playback Ended](https://developers.telnyx.com/api-reference/callbacks/conference-playback-ended.md) - [Conference Playback Started](https://developers.telnyx.com/api-reference/callbacks/conference-playback-started.md) - [Conference Recording Saved](https://developers.telnyx.com/api-reference/callbacks/conference-recording-saved.md) - [Conference Speak Ended](https://developers.telnyx.com/api-reference/callbacks/conference-speak-ended.md) - [Conference Speak Started](https://developers.telnyx.com/api-reference/callbacks/conference-speak-started.md) ### Conference Commands - [List conferences](https://developers.telnyx.com/api-reference/conference-commands/list-conferences.md): Lists conferences. Conferences are created on demand, and will expire after all participants have left the conference or after 4 hours regardless of the number… - [Create conference](https://developers.telnyx.com/api-reference/conference-commands/create-conference.md): Create a conference from an existing call leg using a `call_control_id` and a conference name. Upon creating the conference, the call will be automatically bri… - [Retrieve a conference](https://developers.telnyx.com/api-reference/conference-commands/retrieve-a-conference.md): Retrieve an existing conference - [Hold conference participants](https://developers.telnyx.com/api-reference/conference-commands/hold-conference-participants.md): Hold a list of participants in a conference call - [Join a conference](https://developers.telnyx.com/api-reference/conference-commands/join-a-conference.md): Join an existing call leg to a conference. Issue the Join Conference command with the conference ID in the path and the `call_control_id` of the leg you wish t… - [Leave a conference](https://developers.telnyx.com/api-reference/conference-commands/leave-a-conference.md): Removes a call leg from a conference and moves it back to parked state. - [Mute conference participants](https://developers.telnyx.com/api-reference/conference-commands/mute-conference-participants.md): Mute a list of participants in a conference call - [Play audio to conference participants](https://developers.telnyx.com/api-reference/conference-commands/play-audio-to-conference-participants.md): Play audio to all or some participants on a conference call. - [Conference recording pause](https://developers.telnyx.com/api-reference/conference-commands/conference-recording-pause.md): Pause conference recording. - [Conference recording resume](https://developers.telnyx.com/api-reference/conference-commands/conference-recording-resume.md): Resume conference recording. - [Conference recording start](https://developers.telnyx.com/api-reference/conference-commands/conference-recording-start.md): Start recording the conference. Recording will stop on conference end, or via the Stop Recording command. - [Conference recording stop](https://developers.telnyx.com/api-reference/conference-commands/conference-recording-stop.md): Stop recording the conference. - [Speak text to conference participants](https://developers.telnyx.com/api-reference/conference-commands/speak-text-to-conference-participants.md): Convert text to speech and play it to all or some participants. - [Stop audio being played on the conference](https://developers.telnyx.com/api-reference/conference-commands/stop-audio-being-played-on-the-conference.md): Stop audio being played to all or some participants on a conference call. - [Unhold conference participants](https://developers.telnyx.com/api-reference/conference-commands/unhold-conference-participants.md): Unhold a list of participants in a conference call - [Unmute conference participants](https://developers.telnyx.com/api-reference/conference-commands/unmute-conference-participants.md): Unmute a list of participants in a conference call - [Update conference participant](https://developers.telnyx.com/api-reference/conference-commands/update-conference-participant.md): Update conference participant supervisor_role - [End a conference](https://developers.telnyx.com/api-reference/conference-commands/end-a-conference.md): End a conference and terminate all active participants. - [Gather DTMF using audio prompt in a conference](https://developers.telnyx.com/api-reference/conference-commands/gather-dtmf-using-audio-prompt-in-a-conference.md): Play an audio file to a specific conference participant and gather DTMF input. - [Send DTMF to conference participants](https://developers.telnyx.com/api-reference/conference-commands/send-dtmf-to-conference-participants.md): Send DTMF tones to one or more conference participants. - [List conference participants](https://developers.telnyx.com/api-reference/conference-commands/list-conference-participants.md): Lists conference participants - [Retrieve a conference participant](https://developers.telnyx.com/api-reference/conference-commands/retrieve-a-conference-participant.md): Retrieve details of a specific conference participant by their ID or label. - [Update a conference participant](https://developers.telnyx.com/api-reference/conference-commands/update-a-conference-participant.md): Update properties of a conference participant.