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
The whole flow is visible: the tool call arrives, the filler messages fire, the countdown ticks, the response goes back.
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
The AI Assistant calls the webhook as a sync tool. The webhook sleeps for WEBHOOK_DELAY_SECONDS (default 12). During that delay, the AI Assistant speaks the filler messages. The browser dashboard receives real-time events via SSE.
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
The WEBHOOK_DELAY_SECONDS controls how long the webhook waits before responding. Set it to 12-15 seconds so multiple filler messages trigger during the delay.
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.
Start ngrok first (the setup script needs your public URL):
ngrok http 5000
Copy the HTTPS URL, then run setup:
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.
Manual Setup (Alternative)
If you prefer to configure everything in Mission Control manually:
- Go to AI → Assistants in the Telnyx Portal
- Create a new assistant with telephony enabled
- Add a sync webhook tool:
- Name: check_order_status - Description: "Check the status of a customer order by order ID" - Parameters: order_id (string, required) - Webhook URL: https://<id>.ngrok.io/webhook/order-status - Timeout: 30000ms (default 5000ms is too short)
- Under the Filler Messages tab, add:
| Type | Content | Timing |
|---|---|---|
request_start | "Let me look that up for you." | immediate |
request_response_delayed | "Still working on this, one moment please." | 5000ms |
request_response_delayed | "Almost there, thanks for your patience." | 15000ms |
- Assign a phone number to the assistant
Step 3: Understand the Code
Everything lives in app.py (124 lines). Here's what each piece does.
The Filler Message Configuration
The filler messages are defined as a config array:
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 calledrequest_response_delayed— fires aftertiming_msmilliseconds if the webhook hasn't responded yet
The Webhook Endpoint
The webhook receives the sync tool call, broadcasts events to the dashboard, sleeps for the configured delay, then returns mock order data:
@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 when the call arrives, filler_message for each configured filler, countdown every second, and response_sent when the webhook returns.
The SSE Dashboard
The dashboard uses Server-Sent Events for real-time updates. 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)
The SSE endpoint streams events to the browser with a 30-second keepalive:
@app.route("/events")
def events():
q = queue.Queue(maxsize=100)
with clients_lock:
clients.append(q)
def stream():
yield f"data: {json.dumps({'type': 'connected', 'timestamp': time.time(), 'delay_seconds': WEBHOOK_DELAY_SECONDS})}\n\n"
while True:
try:
msg = q.get(timeout=30)
yield msg
except queue.Empty:
yield ": keepalive\n\n"
return Response(stream(), mimetype="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
The Mock Orders
Three orders for testing:
MOCK_ORDERS = {
"12345": {"order_id": "12345", "status": "shipped", "carrier": "FedEx", "tracking": "FX-98765", "eta": "2026-07-20", "items": ["Wireless Headset", "USB-C Cable"]},
"67890": {"order_id": "67890", "status": "processing", "carrier": "pending", "tracking": "pending", "eta": "2026-07-25", "items": ["Desk Lamp"]},
"11111": {"order_id": "11111", "status": "delivered", "carrier": "UPS", "tracking": "1Z999AA10123456784", "eta": "2026-07-14", "items": ["Keyboard", "Mouse", "Monitor Stand"]},
}
Call and ask about order 12345, 67890, or 11111 to see different statuses.
Step 4: Run the Demo
Start the Flask server:
python app.py
Open http://localhost:5000 in a browser — you'll see the split-screen dashboard with:
- Left panel: Call timeline showing tool calls, filler message indicators, and responses
- Right panel: Server logs with request/response JSON and delay countdown
Make sure ngrok is running and the webhook URL is configured in your AI Assistant's tool:
ngrok http 5000
Step 5: Call and Watch
Call your AI Assistant's phone number. Ask:
"What's the status of my order 12345?"
Watch the dashboard and listen to the call:
- 0 seconds — the AI Assistant says "Let me look that up for you." and calls the webhook
- 5 seconds — the AI Assistant says "Still working on this, one moment please."
- 12 seconds — the webhook responds with the order status
- After response — the AI Assistant reads back the order details: "Your order 12345 has shipped via FedEx, tracking number FX-98765, estimated delivery July 20th."
The dashboard shows the tool call arriving, the filler messages firing at their configured times, the countdown ticking, and the response going back — all in real time.
Test Without a Phone Call
You can test the webhook endpoint directly:
curl -X POST http://localhost:5000/webhook/order-status \
-H "Content-Type: application/json" \
-d '{"order_id": "12345"}'
Open the dashboard in a browser first to see the events stream in real time.
Customizing the Filler Messages
The filler message configuration is the control surface. 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},
]
Customize the messages for your brand:
{"type": "request_start", "content": "Let me check on that for you right away.", "timing_ms": 0}
{"type": "request_response_delayed", "content": "I'm pulling up your account details now.", "timing_ms": 5000}
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}
{"type": "request_response_delayed", "content": "Almost done, just waiting for final confirmation.", "timing_ms": 15000}
For a booking tool:
{"type": "request_start", "content": "Let me check availability for those dates.", "timing_ms": 0}
{"type": "request_response_delayed", "content": "I'm searching across all our properties now.", "timing_ms": 5000}
Going to Production
This example uses mock data and intentional delays. For 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
- Error handling — add timeout handling and retry logic for upstream API failures
- Monitoring — add structured logging and alerting
- Filler message tuning — adjust timing and content based on real webhook response times
- Timeout alignment — set the tool
timeout_ms(default 5000ms) to at least 30000ms so the webhook has time to respond before the assistant gives up