When an AI voice assistant calls a sync webhook tool — to look up an order, query a database, check inventory — the caller hears silence. Five seconds of dead air on a phone call feels like thirty. Hang up. Call again. Complaint filed.
Filler messages solve this. Configured per-tool in Telnyx Mission Control, they let the AI Assistant speak scripted phrases while waiting for the webhook to respond: "Let me look that up for you." at 0 seconds, "Still working on this, one moment please." at 5 seconds, "Almost there, thanks for your patience." at 15 seconds. The caller stays engaged. The webhook takes the time it needs. No dead air.
This walkthrough builds a complete demo: a Flask webhook server with a live split-screen dashboard that shows the webhook call, the filler messages firing, and the countdown in real time. 124 lines of Python, one AI Assistant, one phone number, one dashboard — and zero silence.
What You'll Build
A webhook server that an AI Assistant calls when a caller asks about an order:
- Caller dials the AI Assistant — a voice AI agent configured in Mission Control
- Caller asks a question — "What's the status of my order 12345?"
-
AI Assistant calls the webhook — a sync tool call to
/webhook/order-status - Webhook delays — intentionally sleeps for 12 seconds (configurable) to simulate a slow backend
- Filler messages fire — the AI Assistant speaks scripted phrases at 0s, 5s, and 15s while the webhook is processing
- Webhook responds — returns mock order status as JSON
- AI Assistant reads the result — "Your order 12345 has shipped via FedEx, tracking number FX-98765, estimated delivery July 20th."
- Dashboard shows everything — a split-screen browser UI with the call timeline on the left and server logs on the right, updated in real time via Server-Sent Events
Why Filler Messages Matter
Every voice AI developer hits the same problem: sync webhook tool calls take time. A database query takes 2 seconds. A third-party API takes 5 seconds. A complex lookup takes 10 seconds. On a phone call, that's dead air — and dead air is the enemy of conversation.
Without filler messages, developers build workarounds:
- Background music — doesn't tell the caller what's happening
- "Please hold" TTS — fires once, then silence resumes
- Async tools — more complex to build, don't work for all use cases
- Faster webhooks — not always possible when you depend on external systems
Filler messages are the carrier-grade solution: the AI Assistant speaks configurable phrases at configurable intervals while the webhook processes. The caller knows the system is working. The developer doesn't have to build a workaround.
Prerequisites
- Python 3.8+
- A Telnyx account with funded balance
- A Telnyx API key
- A Telnyx phone number assigned to an AI Assistant
- An AI Assistant with telephony enabled
- ngrok for exposing your local server
The Architecture
Caller dials AI Assistant
│
▼
┌──────────────────────┐
│ Telnyx AI Assistant │
│ (Mission Control) │
└────────┬─────────────┘
│ sync tool call
▼
┌──────────────────────┐ SSE ┌──────────────────┐
│ Flask Webhook Server │──────────►│ Browser Dashboard │
│ POST /webhook/ │ │ Split-screen UI │
│ order-status │ │ (timeline + logs) │
└──────────────────────┘ └──────────────────┘
│
delays N seconds,
then returns mock
order status
Step 1: Clone and Configure
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-assistant-filler-messages-demo-python
cp .env.example .env
pip install -r requirements.txt
Edit .env with your credentials:
TELNYX_API_KEY=your_telnyx_api_key_here
WEBHOOK_DELAY_SECONDS=12
PORT=5000
Step 2: Set Up the AI Assistant
The repo includes a setup.py script that creates the AI Assistant, adds the webhook tool with filler messages, and assigns your phone number — all via the Telnyx API.
ngrok http 5000
python setup.py https://abc123.ngrok.io
The script will:
- List your active phone numbers — pick one
- Create an AI Assistant named "Filler Messages Demo" with Claude Haiku 4.5
- Add a sync webhook tool (
check_order_status) pointing to your ngrok URL - Configure the three filler messages on the tool
- Assign your phone number to the assistant's backing TeXML app
Important: The API sets the filler message values, but playback on calls requires configuring them through the Mission Control UI. After running setup.py, open the tool's Filler Messages tab in Mission Control and verify the three messages are present.
Step 3: Understand the Code
Everything lives in app.py (124 lines). Here's what each piece does.
The Filler Message Configuration
FILLER_CONFIG = [
{"type": "request_start", "content": "Let me look that up for you.", "timing_ms": 0},
{"type": "request_response_delayed", "content": "Still working on this, one moment please.", "timing_ms": 5000},
{"type": "request_response_delayed", "content": "Almost there, thanks for your patience.", "timing_ms": 15000},
]
Two types:
-
request_start— fires immediately when the webhook tool is called -
request_response_delayed— fires aftertiming_msmilliseconds if the webhook hasn't responded yet
The Webhook Endpoint
@app.route("/webhook/order-status", methods=["POST"])
def webhook_order_status():
body = request.get_json(silent=True) or {}
order_id = body.get("order_id", "unknown")
broadcast("tool_call_received", {"order_id": order_id, "request_body": body, "delay_seconds": WEBHOOK_DELAY_SECONDS})
for filler in FILLER_CONFIG:
if filler.get("timing_ms", 0) / 1000 < WEBHOOK_DELAY_SECONDS:
broadcast("filler_message", {"content": filler["content"], "filler_type": filler["type"], "timing_ms": filler.get("timing_ms", 0)})
for elapsed in range(1, WEBHOOK_DELAY_SECONDS + 1):
time.sleep(1)
broadcast("countdown", {"elapsed": elapsed, "total": WEBHOOK_DELAY_SECONDS, "remaining": WEBHOOK_DELAY_SECONDS - elapsed})
order = MOCK_ORDERS.get(order_id, {"order_id": order_id, "status": "not_found", "error": "Order not found"})
return jsonify({"result": order}), 200
The webhook broadcasts four event types to the dashboard: tool_call_received, filler_message, countdown, and response_sent.
The SSE Dashboard
Real-time updates via Server-Sent Events. Each connected browser gets a queue, and the broadcast() function pushes events to all clients:
def broadcast(event_type, data):
payload = json.dumps({"type": event_type, "timestamp": time.time(), **data})
msg = f"event: {event_type}\ndata: {payload}\n\n"
dead = []
with clients_lock:
for q in clients:
try:
q.put_nowait(msg)
except queue.Full:
dead.append(q)
for q in dead:
clients.remove(q)
Step 4: Run the Demo
python app.py
Open http://localhost:5000 to see the split-screen dashboard. Call your AI Assistant's phone number. Ask:
"What's the status of my order 12345?"
Watch the dashboard and listen to the call:
- 0s — the AI Assistant says "Let me look that up for you." and calls the webhook
- 5s — the AI Assistant says "Still working on this, one moment please."
- 12s — the webhook responds with the order status
- After response — the AI Assistant reads back the order details
Customizing the Filler Messages
Different timings and content produce very different caller experiences:
Add a 10-second filler:
FILLER_CONFIG = [
{"type": "request_start", "content": "Let me look that up for you.", "timing_ms": 0},
{"type": "request_response_delayed", "content": "Still working on this, one moment please.", "timing_ms": 5000},
{"type": "request_response_delayed", "content": "I'm checking with the shipping carrier now.", "timing_ms": 10000},
{"type": "request_response_delayed", "content": "Almost there, thanks for your patience.", "timing_ms": 15000},
]
Use context-aware fillers for different tools:
For a payment processing tool:
{"type": "request_start", "content": "Let me process that payment for you.", "timing_ms": 0}
{"type": "request_response_delayed", "content": "I'm confirming the transaction with your bank.", "timing_ms": 5000}
Going to Production
-
Real data source — replace
MOCK_ORDERSwith actual database or API queries - Remove artificial delay — the delay exists only for demo purposes; real webhooks should respond as fast as possible
- Webhook verification — validate Telnyx webhook signatures
-
Timeout alignment — set the tool
timeout_ms(default 5000ms) to at least 30000ms
Frequently Asked Questions
What are AI Assistant filler messages?
Filler messages are scripted phrases the AI Assistant speaks while waiting for a sync webhook tool to respond. They eliminate dead air on the call and keep the caller informed. They're configured per-tool in Mission Control.
How are filler messages different from TTS hold music?
Filler messages are spoken by the AI Assistant in its own voice, as part of the conversation. Hold music is audio played while the call is on hold. Filler messages feel like the assistant is still there and working on your request.
What happens if the webhook responds before the first filler message fires?
If the webhook responds before the first request_response_delayed message's timing_ms, that message doesn't fire. The request_start message always fires immediately when the tool is called.
How do I configure filler messages?
Filler messages are configured per-tool in Mission Control. Open the AI Assistant, edit the webhook tool, and go to the Filler Messages tab. You can also set them via the API (setup.py does this), but playback on calls requires the Mission Control UI configuration.
Top comments (0)