Personal voice assistant using Twilio Voice and OpenAI's Realtime API for phone-based AI conversations.
sequenceDiagram
participant Caller
participant Twilio
participant TwilioFunction as Twilio Function<br/>(Allowlist)
participant Assistant as your-tunnel-name.trycloudflare.com
participant OpenAI as OpenAI Realtime API
Caller->>Twilio: Dials phone number
Twilio->>TwilioFunction: Incoming call webhook
alt Number in allowlist
TwilioFunction->>Assistant: POST /incoming-call<br/>(with Twilio signature)
Assistant->>Assistant: Validate signature
Assistant->>Assistant: Generate WebSocket token
Assistant-->>TwilioFunction: Return TwiML with<br/>WebSocket URL + token
TwilioFunction-->>Twilio: TwiML response
Twilio->>Assistant: Connect to /media-stream<br/>(with token)
Assistant->>Assistant: Validate token
Assistant->>OpenAI: Establish WebSocket
loop Audio streaming
Twilio->>Assistant: Audio from caller
Assistant->>OpenAI: Forward audio
OpenAI->>Assistant: AI response audio
Assistant->>Twilio: Forward to caller
end
else Number not in allowlist
TwilioFunction-->>Twilio: Reject call
Twilio-->>Caller: Call rejected
end
This application uses three layers of security to protect against unauthorized access:
- Runs on Twilio's infrastructure before reaching your server
- Only approved phone numbers can proceed
- See
twilio/allowlist-function.jsfor implementation
- Validates all webhook requests from Twilio
- Ensures requests are authentic and haven't been tampered with
- Uses HMAC-SHA1 with your
TWILIO_AUTH_TOKEN
- Single-use tokens generated for each call
- 60-second expiration window
- Prevents unauthorized WebSocket connections
- Python 3.13+
- uv for dependency management
- Twilio account with a voice-capable phone number
- OpenAI API key with Realtime API access
- cloudflared for local development tunneling
- Docker (optional, for containerized deployment)
- Fly.io account (optional, for production deployment)
cp .env.example .envEdit .env and configure:
OPENAI_API_KEY- Your OpenAI API keyTWILIO_AUTH_TOKEN- Your Twilio Auth Token (found in Twilio Console)ZAPIER_MCP_URL- Zapier MCP server URL (default:https://mcp.zapier.com/api/mcp/mcp)ZAPIER_MCP_PASSWORD- Zapier API key in base64 format (get from Zapier MCP Developer)ASSISTANT_INSTRUCTIONS- AI assistant personality, behavior, and tool usage instructionsVOICE- OpenAI voice name (e.g.,alloy,shimmer,nova)PORT- Server port (default: 5050)TEMPERATURE- AI temperature (default: 0.8)
Note: WEBHOOK_URL and ALLOWED_NUMBERS are only used in the Twilio Function (see Layer 1 security below), not in your local application.
Option A: Quick temporary tunnel (random URL):
make tunnel-quickCopy the forwarding URL (e.g., https://xyz.trycloudflare.com).
Option B: Named tunnel with stable domain (one-time setup):
# 1. Authenticate with Cloudflare
cloudflared tunnel login
# 2. Create a named tunnel
cloudflared tunnel create assistant
# 3. Route a DNS hostname (replace with your domain)
cloudflared tunnel route dns assistant assistant.yourdomain.com
# 4. Create ~/.cloudflared/assistant.yml with your tunnel ID and domain
# 5. Run the tunnel
make tunnelCreate the Function:
- In Twilio Console, go to Functions & Assets > Services
- Create a new Service (e.g., "voice-assistant-auth")
- Add a new Function with path
/incoming-call - Copy the code from
twilio/allowlist-function.js - In Environment Variables, add:
ALLOWED_NUMBERS- Comma-separated phone numbers (e.g.,+15551234567,+15559876543)WEBHOOK_URL- Your assistant URL (e.g.,https://your-tunnel-name.trycloudflare.com/incoming-call)
- Deploy the service
Configure Your Phone Number:
- Navigate to Phone Numbers > Manage > Active Numbers
- Select your number
- Set A call comes in to Function: Select your deployed function
- Save
uv run python main.pyCall your Twilio number to talk with the assistant.
Build and run locally:
docker compose upRun with cloudflared tunnel (dev profile):
docker compose --profile dev upInitial setup:
# Install flyctl
brew install flyctl
# Authenticate
fly auth login
# Launch app (generates fly.toml and creates app, but doesn't deploy)
fly launch --no-deployConfigure environment:
Set secrets:
fly secrets set OPENAI_API_KEY=your_key_here
fly secrets set TWILIO_AUTH_TOKEN=your_token_here
fly secrets set ZAPIER_MCP_PASSWORD=your_zapier_api_key_base64Non-secret environment variables (VOICE, TEMPERATURE, ASSISTANT_INSTRUCTIONS, ZAPIER_MCP_URL) are configured in fly.toml.
Deploy:
fly deployGet your app URL:
fly statusYour webhook URL will be: https://[your-app-name].fly.dev/incoming-call
Use this URL as your WEBHOOK_URL in the Twilio Function.
Scale to single machine (optional):
fly scale count 1 -yCustom domain setup (optional):
- Get your Fly IP addresses:
fly ips list-
In your DNS provider (e.g., Cloudflare), add DNS records for your custom domain:
- A record: Point to the IPv4 address shown in
fly ips list - AAAA record: Point to the IPv6 address shown in
fly ips list
- A record: Point to the IPv4 address shown in
-
Add the custom domain to Fly (triggers Let's Encrypt certificate):
fly certs add your-domain.com- Check certificate status:
fly certs show your-domain.com- Once issued, update your Twilio Function's
WEBHOOK_URLto use your custom domain:- Example:
https://your-domain.com/incoming-call
- Example:
View logs:
fly logsThis assistant integrates with Zapier's MCP server, which connects to multiple services:
- Todoist: Task management and reminders
- Gmail: Email search
-
Get Zapier MCP API Key:
- Go to Zapier MCP Developer
- Generate an API key
- Set secret as
ZAPIER_MCP_PASSWORD
-
Configure in
.env:ZAPIER_MCP_URL=https://mcp.zapier.com/api/mcp/mcp ZAPIER_MCP_PASSWORD=your_zapier_api_key_base64
-
The MCP configuration is set in
main.py:{ "type": "mcp", "server_label": "zapier", "server_url": ZAPIER_MCP_URL, "headers": { "Authorization": f"Bearer {ZAPIER_MCP_PASSWORD}" }, "require_approval": "never" }
Once configured, you can use natural language for:
Todoist Tasks:
- "Add buy milk to my todo list"
- "What tasks do I have today?"
- "Mark task as complete"
- "What's due tomorrow?"
Gmail Search:
- "Search my email for messages about project updates"
- "Find emails from John sent this week"
When fetching today's tasks, the assistant uses the Todoist API with the filter=today parameter:
GET https://api.todoist.com/rest/v2/tasks?filter=today
This ensures accurate results when users ask "what do I have to do today?" or similar queries.
- Real-time voice conversation with OpenAI
- Natural interrupt handling and AI preemption
- Bidirectional audio streaming between Twilio and OpenAI
- Task management via Zapier MCP (Todoist integration)
- Email search via Zapier MCP (Gmail integration)