Build a Natural Language IVR with Telnyx Call Control and AI Inference Telnyx has published a code example demonstrating a natural language IVR system that replaces traditional phone trees with conversational AI. The Python/Flask app uses Telnyx Call Control and AI inference to generate dynamic greetings and route callers to the correct department based on speech input, with fallback mechanisms and webhook signature verification. Nobody likes phone trees. "Press 1 for billing, press 2 for support." Miss an option? Start over. It is friction at its worst. The voice-ivr-with-agent-backend example replaces that with a natural language conversation. Callers just say what they need, and the app routes them to the right department. Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend A Python/Flask app that handles inbound calls with a conversational IVR: php Inbound Call - answer with Call Control - look up menu config from KV - LLM generates a dynamic greeting - gather speech — caller says what they need - LLM routes intent to a department - transfer call The app combines four Telnyx primitives: answer , speak , gather using speech , transfer telnyx.ai.openai.chat.completions.create for greetings and intent routing IVRAgent class that tracks call state, turn count, and retry logicInstead of a hardcoded "Press 1 for billing," the app generates a conversational greeting from the KV config: php def generate dynamic menu prompt menu config: dict - str: departments = menu config.get "departments", dept list = "\n".join f"- {d 'name' }: {d 'description' }" for d in departments return f"You are an IVR assistant for {menu config 'business name' }. " f"Available departments:\n{dept list}\n\n" f"Greet the caller briefly and ask how you can help. " f"Keep it conversational and under 2 sentences." The LLM generates the greeting through the OpenAI-compatible Telnyx Inference binding. If it fails, the app falls back to a static greeting from the KV config. When the caller speaks, the transcription is passed to route intent with llm . The LLM is instructed to respond with only the department name for reliable parsing: completion = telnyx.ai.openai.chat.completions.create model="telnyx-llm", messages= {"role": "system", "content": system prompt}, {"role": "user", "content": user input}, , max tokens=20, temperature=0.1, intent = completion.choices 0 .message.content.strip .lower If the LLM fails or returns "unknown," the app falls back to keyword matching. After max turns 3 by default , the call transfers to a default operator. The gather using speech primitive plays a prompt and captures the caller's speech in one call: telnyx.Call.gather using speech call control id, payload=prompt, voice="female-en-US", language="en-US", max duration=15, Every webhook request is verified with Ed25519 signature verification: telnyx.Webhook.construct event payload, signature, timestamp, TELNYX PUBLIC KEY This prevents spoofed requests from triggering call actions. The IVRAgent class manages each call: on connect : fetch menu config, generate LLM greeting, speak, gather on gather ended speech : route intent via LLM, transfer or retry turn count and max turns before falling back to a default transfer PUT /api/menu-config/