Back to Blog
Voice & Telephony

Building a Programmable Voice IVR with Python and Telnyx

Learn how to build a production voice IVR system that handles inbound calls, menu navigation, and call routing. We cover Telnyx API integration, webhooks, state machines, and deployment.

Sep 25, 2026
13 min read
By Brian Shimkus
Building a Programmable Voice IVR with Python and Telnyx

The Problem: Customer Support Over Phone

Your support team is drowning in repetitive calls. Customers call asking "What's the status of my order?" or "How do I reset my password?" These calls take 5 minutes to handle. Your team answers 100 calls a day. That's 500 minutes of labor on questions an automated system could answer in 30 seconds.

You need a voice IVR: Interactive Voice Response. A phone system that answers calls, plays prompts, listens to DTMF tones (button presses), and routes calls intelligently. No building a call center. Just a smart phone system.

Deskline is an IVR platform built with Python and Telnyx. It handles inbound calls, plays menu options, collects user input, and routes or transfers calls. This post walks through the architecture and how to build one.

The Architecture

A voice IVR has four moving pieces: the phone network (Telnyx), webhooks receiving call events, a state machine managing call flow, and business logic connecting to your backend.

Telnyx: The Phone Network

Telnyx provides a phone number and handles the phone network side. Inbound calls trigger webhooks to your server. You respond with commands telling Telnyx what to do: play audio, collect digits, transfer to another number, etc.

Webhooks: The Connection

When a caller presses digits or hangs up, Telnyx POSTs to your webhook. Your server responds with the next action. It's request-response: call event triggers POST, you send back commands.

State Machine: Call Flow Control

Each call is a state machine. "Greeting" state plays a welcome message and asks for input. "Menu" state processes the caller's choice. "Routing" state transfers to an agent. You track state in a database tied to the call ID.

Business Logic: The Backend

Your app queries a database to look up order status, validate customer info, or decide routing logic. The IVR acts as a voice interface to your backend systems.

Setting Up Telnyx

First, you need a Telnyx account and a phone number. Telnyx gives you an API key. You configure a webhook URL: when Telnyx receives a call, it POSTs to your server with call details.

1. Buy a phone number from Telnyx dashboard
2. Generate an API key for authentication
3. Set webhook URL in your Telnyx app settings
4. Telnyx POSTs to your webhook on call events
5. Your server responds with Telnyx commands (play audio, collect digits, etc.)

The webhook receives a POST with call ID, caller phone, and event type. Your job: decide what to do next and return commands as JSON.

Building the State Machine

Each state handles a specific part of the call flow. The greeting state plays a welcome message. The menu state waits for button presses. The routing state transfers to a live agent.

from enum import Enum
from pydantic import BaseModel

class CallState(str, Enum):
    GREETING = "greeting"
    MENU = "menu"
    COLLECTING_ACCOUNT_NUMBER = "collecting_account"
    RETRIEVING_ORDER = "retrieving_order"
    ROUTING = "routing"
    TRANSFER = "transfer"

class CallSession(BaseModel):
    call_id: str
    caller_phone: str
    current_state: CallState
    collected_digits: str = ""
    account_number: str = ""

# Database stores CallSession keyed by call_id
# When webhook fires, lookup call_id, read current_state
# Execute state handler, update database with new state

Handling Webhook Events

Telnyx sends webhooks for three main events: call initiated, DTMF digits received, call hangup. Each webhook includes the call ID, which you use to look up the call session and determine what to do next.

@app.post("/webhooks/telnyx")
async def handle_telnyx_webhook(payload: dict):
    call_id = payload.get("data")["payload"]["call_id"]
    event_type = payload.get("data")["payload"]["type"]

    # Look up the call session
    session = db.query(CallSession).filter_by(call_id=call_id).first()
    if not session:
        session = CallSession(call_id=call_id, current_state=CallState.GREETING)
        db.add(session)
        db.commit()

    # Route based on current state
    if session.current_state == CallState.GREETING:
        commands = handle_greeting(call_id)
    elif session.current_state == CallState.MENU:
        digits = payload.get("data")["payload"].get("digits")
        commands = handle_menu_selection(call_id, digits)
    elif event_type == "call.hangup":
        db.delete(session)
        db.commit()
        return {"status": "ok"}

    # Send commands back to Telnyx
    telnyx.post(f"/calls/{call_id}/actions", data=commands)
    return {"status": "ok"}

The request-response pattern is fast: Telnyx calls your webhook, you process in milliseconds and respond with the next Telnyx command. The caller hears the response almost instantly.

Playing Audio and Collecting Input

The two main commands you send to Telnyx are: "play audio" (TTS or pre-recorded files) and "gather" (listen for DTMF digits). Combine them to prompt the caller and collect their response.

def handle_greeting(call_id: str):
    return {
        "calls": [{
            "call_id": call_id,
            "actions": [
                {
                    "type": "speak",
                    "payload": {
                        "language": "en-US",
                        "text": "Welcome to our support line. Press 1 for order status, 2 for billing, 3 for technical support."
                    }
                },
                {
                    "type": "gather_dtmf",
                    "payload": {
                        "max_digits": 1,
                        "timeout_millis": 5000
                    }
                }
            ]
        }]
    }

The "speak" action uses text-to-speech (TTS) to play the prompt. "gather_dtmf" listens for button presses. When the caller presses a digit, Telnyx sends it in the next webhook as the digits field.

Connecting to Your Backend

The real power comes when you connect the IVR to your backend. The caller presses "1" for order status. Your IVR collects their order number, queries your database, and speaks the result back.

def handle_order_lookup(call_id: str, order_number: str):
    # Query your backend for order status
    order = db.query(Order).filter_by(order_id=order_number).first()
    if not order:
        status_text = "Order not found. Please try again."
    else:
        status_text = f"Your order {order_number} is {order.status}. It will arrive by {order.delivery_date}."

    # Return TTS response to caller
    return {
        "calls": [{
            "call_id": call_id,
            "actions": [
                {
                    "type": "speak",
                    "payload": {
                        "language": "en-US",
                        "text": status_text
                    }
                },
                {
                    "type": "speak",
                    "payload": {
                        "text": "Press 1 to speak with an agent, or hang up."
                    }
                }
            ]
        }]
    }

The IVR becomes a voice interface to your business logic. Database queries, API calls, business rules all work the same way. The difference is the caller hears responses spoken aloud instead of reading them on a screen.

Production Considerations

  • •Signature verification: Telnyx signs every webhook. Verify signatures in your handler to prevent spoofed calls.
  • •Call recording: Record calls for compliance and quality review. Enable recording in Telnyx settings and store securely.
  • •Error handling: Network timeouts happen. Design fallback messages and graceful error paths.
  • •Call timeout: If a caller is silent too long, hang up gracefully. Telnyx handles this with timeout settings.
  • •Agent handoff: Transfer to a live agent when needed using the "transfer" action. Map to your desk phone or call center.
  • •Billing and monitoring: Telnyx charges per minute. Monitor spending and set alerts. Log call metrics for analytics.

See It in Action

Deskline is a full production voice IVR handling real calls end-to-end. Check out the case study for the complete architecture, including call state persistence, error recovery, and integration with backend systems.

Deskline Case Study

Key Takeaways

  • ✓Voice IVRs answer calls, play prompts, collect input, and route intelligently. No call center needed.
  • ✓Telnyx handles the phone network. You build a webhook handler and state machine.
  • ✓Request-response architecture: Telnyx POSTs events, you respond with commands.
  • ✓Connect the IVR to your backend: database queries, API calls, business logic all work through voice.
  • ✓Production IVRs need signature verification, error handling, call recording, and agent handoff.

Building voice systems or need help with telephony integration? Let me know.

Contact Me