Imagine calling a phone number, picking a genre — mystery, sci-fi, fantasy, horror, or romance — and listening to an AI narrate a story that adapts to your choices in real time. Press 1 to open the creaking door. Press 2 to check the window. The story branches, the AI continues, and every call is a new adventure.
This is the AI Phone Story Hotline — a 104-line Python app built with Telnyx Call Control and AI Inference. No game engine, no branching script, no pre-written dialogue trees. The AI generates the story as you go, and your phone keypad shapes what happens next.
In this walkthrough, you'll build it from scratch. Clone the repo, configure a phone number, and deploy in minutes.
What You'll Build
A phone number that anyone can call to start an interactive story:
- Caller dials in — Telnyx answers and greets them with a genre menu
- Caller picks a genre — press 1–5 for mystery, sci-fi, fantasy, horror, or romance
- AI generates chapter one — a 3–4 sentence story segment ending with two choices
- Caller chooses — press 1 or 2, or speak the choice aloud
- Story continues — AI generates the next chapter based on the choice, keeping full conversation context
- After 5 chapters — the AI brings the story to a satisfying ending
The whole interaction is voice-driven: Text-to-Speech reads each chapter, and the caller responds with DTMF keypresses or speech. The AI model (Llama 3.3 70B via Telnyx AI Inference) handles the storytelling, and Call Control handles the telephony.
Why This Is Interesting
Most AI phone demos are business use cases — book a table, qualify a lead, reset a password. This one is different. It's creative AI — the model is writing fiction in real time, branching based on human input, and delivering it over a phone call. The storytelling format (short chapters, two choices each) keeps the AI responses tight and the call engaging.
It also demonstrates a pattern that works for any conversational AI app: webhook-driven state machine + LLM with conversation memory + voice I/O. Once you see how the story hotline works, you can swap the storytelling prompt for any domain — a choose-your-own-adventure onboarding flow, an interactive quiz, a branching training simulation.
Prerequisites
- Python 3.8+
- A Telnyx account with funded balance
- A Telnyx API key
- A Telnyx phone number with voice enabled
- A Call Control Application configured with your webhook URL
- ngrok for exposing your local server to Telnyx webhooks
The Architecture
Phone Call
│
▼
Telnyx Call Control (webhook events)
│
▼
Flask app (app.py, 104 lines)
│
├──► call.initiated → answer the call
├──► call.answered → TTS greeting + genre menu
├──► call.gather.ended → DTMF/speech input → AI Inference
├──► call.speak.ended → gather next choice → AI Inference
└──► call.hangup → cleanup session
│
▼
Telnyx AI Inference (Llama 3.3 70B)
│
▼
Story chapter (TTS back to caller)
The app is a state machine driven by Telnyx webhook events. Each event triggers the next action — answer, speak, gather, or hang up. The AI Inference call sits in the middle, generating story chapters from the running conversation history.
Step 1: Clone and Configure
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-phone-story-hotline-python
cp .env.example .env
pip install -r requirements.txt
Edit .env with your credentials:
TELNYX_API_KEY=KEY0123456789ABCDEF # from portal.telnyx.com/api-keys
TELNYX_PUBLIC_KEY= # from portal.telnyx.com/api-keys (public key)
STORY_NUMBER=+13105551234 # your Telnyx phone number
AI_MODEL=meta-llama/Llama-3.3-70B-Instruct
Step 2: Understand the Code
The entire app is 104 lines in a single file: app.py. Here are the key pieces.
Webhook Signature Verification
Every Telnyx webhook is signed with an Ed25519 key. The app verifies the signature before trusting any event:
@app.route("/webhooks/voice", methods=["POST"])
def handle_voice():
try:
client.webhooks.unwrap(request.get_data(as_text=True), headers=dict(request.headers))
except Exception:
return jsonify({"error": "invalid signature"}), 401
This prevents spoofed webhook calls from triggering call actions.
The State Machine
Each call is tracked in an in-memory dict keyed by call_control_id:
active_calls = {}
The state machine handles five events:
Call initiated — store the call state and answer:
if event_type == "call.initiated" and p.get("direction") == "incoming":
active_calls[ccid] = {"state": "genre_select", "conversation": [], "chapters": 0}
client.calls.actions.answer(ccid)
Call answered — greet the caller with the genre menu using Text-to-Speech:
elif event_type == "call.answered":
client.calls.actions.speak(ccid,
payload="Welcome to Story Hotline! Choose your adventure. Press 1 for Mystery, 2 for Sci-Fi, 3 for Fantasy, 4 for Horror, 5 for Romance.",
voice="female", language_code="en-US")
Speak ended — after TTS finishes, gather the caller's input. During genre selection, gather DTMF digits. During the story, gather speech or DTMF:
elif event_type == "call.speak.ended" and call:
if call["state"] == "genre_select":
client.calls.actions.gather(ccid, input_type="dtmf", timeout_secs=10, min_digits=1, max_digits=1)
else:
client.calls.actions.gather(ccid, input_type="speech dtmf", end_silence_timeout_secs=3, timeout_secs=20, language_code="en-US")
The Storytelling Prompt
When the caller picks a genre, the app builds the system prompt that guides the AI for the rest of the call:
GENRES = {"1": "mystery", "2": "sci-fi", "3": "fantasy", "4": "horror", "5": "romance"}
if call["state"] == "genre_select":
genre = GENRES.get(digits, "mystery")
call["state"] = "story"
call["conversation"] = [{"role": "system", "content":
f"You are an interactive {genre} storyteller on a phone hotline. "
f"Tell a gripping story in short chapters (3-4 sentences each). "
f"End each chapter with exactly TWO choices: 'Press 1 to...' or 'Press 2 to...'. "
f"Make it vivid and cinematic. After 5 chapters, bring the story to a satisfying ending."
}]
story_start = call_inference(call["conversation"] + [{"role": "user", "content": "Begin the story."}])
call["conversation"].append({"role": "assistant", "content": story_start})
client.calls.actions.speak(ccid, payload=story_start, voice="female", language_code="en-US")
The prompt does three things:
- Format constraint — short chapters (3–4 sentences) that fit naturally in a phone call
- Choice structure — exactly two options ending with "Press 1" or "Press 2" so the gather step can capture DTMF
- Ending condition — after 5 chapters, wrap up the story
AI Inference
The call_inference helper sends the full conversation history to Telnyx AI Inference and returns the model's response:
def call_inference(messages, max_tokens=250):
resp = requests.post(INFERENCE_URL,
headers={"Authorization": f"Bearer {TELNYX_API_KEY}", "Content-Type": "application/json"},
json={"model": AI_MODEL, "messages": messages, "max_tokens": max_tokens, "temperature": 0.9, },
timeout=20)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
Temperature is set to 0.9 — high enough for creative storytelling, low enough to keep the narrative coherent. The model is Llama 3.3 70B Instruct, available on Telnyx AI Inference with an OpenAI-compatible API.
The Conversation Loop
Each time the caller makes a choice, the app appends it to the conversation and asks the AI for the next chapter:
elif call["state"] == "story":
choice = digits or speech
call["conversation"].append({"role": "user", "content": f"I choose option {choice}"})
call["chapters"] += 1
if call["chapters"] >= 5:
call["conversation"][-1]["content"] += ". Bring the story to a dramatic conclusion."
continuation = call_inference(call["conversation"])
call["conversation"].append({"role": "assistant", "content": continuation})
client.calls.actions.speak(ccid, payload=continuation, voice="female", language_code="en-US")
After 5 chapters, the app injects a final instruction to bring the story to an end. The AI sees the full conversation history on every call, so it maintains narrative continuity across all chapters.
Step 3: Run the App
Start the Flask server:
python app.py
In a separate terminal, expose your local server with ngrok:
ngrok http 5000
Copy the HTTPS URL and configure it in the Telnx Portal:
- Go to Call Control Applications
- Create or edit your application
- Set the Webhook URL to
https://<your-ngrok-url>.ngrok.app/webhooks/voice
Assign your Telnyx phone number to this Call Control Application if you haven't already.
Step 4: Call and Play
Call your Telnyx number from any phone. You'll hear:
"Welcome to Story Hotline! Choose your adventure. Press 1 for Mystery, 2 for Sci-Fi, 3 for Fantasy, 4 for Horror, 5 for Romance."
Press a key. The AI generates the first chapter and reads it aloud. At the end, you'll hear two choices. Press 1 or 2 — or speak your choice — and the story continues.
After 5 chapters, the AI wraps up the story and the call ends.
Customizing the Experience
The storytelling system prompt is the control surface. Small changes produce very different experiences:
Change the genres:
GENRES = {"1": "noir detective", "2": "space opera", "3": "post-apocalyptic", "4": "gothic horror", "5": "cozy romance"}
Add more choices per chapter:
f"End each chapter with exactly THREE choices: 'Press 1 to...', 'Press 2 to...', or 'Press 3 to...'."
Make it a different format — a quiz, a therapy session, a training scenario:
f"You are an interactive compliance quiz host on a phone hotline. Ask one multiple-choice question per chapter (3-4 sentences). End with 'Press 1 for A, 2 for B, 3 for C.' After 10 questions, score the caller and give feedback."
The pattern — webhook state machine + LLM with conversation history + voice I/O — works for any branching conversational experience.
Going to Production
This example uses in-memory storage for simplicity. For a production deployment:
- Database — replace the in-memory
active_callsdict with Redis or PostgreSQL so call state survives restarts - Concurrency — run the app behind gunicorn with multiple workers
- Error recovery — handle inference timeouts and call failures gracefully with retry or SMS fallback
- Prompt tuning — test different system prompts and temperature settings for your genre
- Rate limiting — protect your webhook endpoint from abuse
- Monitoring — add structured logging and alerting on call success/failure rates