PraisonAI Call
Guide to PraisonAI's voice-based interaction feature enabling AI customer service through phone calls, including setup and tool integration
Turn an Agent into a phone assistant — start the call server and connect a phone number to talk to it over the line.
export OPENAI_API_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY in your shell}"
export NGROK_AUTH_TOKEN="${NGROK_AUTH_TOKEN:?Set NGROK_AUTH_TOKEN in your shell}"
export PRAISONAI_CALL_PUBLIC_BASE="${PRAISONAI_CALL_PUBLIC_BASE:?Set to the wss:// host Twilio will connect to, e.g. wss://your-app.ngrok.io}"
praisonai call --publicWhen --public prints the ngrok URL, copy the wss:// variant back into PRAISONAI_CALL_PUBLIC_BASE before dialling in — the server refuses to derive it from the request Host header.
AI Customer Service
PraisonAI Call is a feature that enables voice-based interaction with AI models through phone calls. This functionality allows users to have natural conversations with AI agents over traditional phone lines.
Installation
Step 1
pip install "praisonai[call]"
export OPENAI_API_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY in your shell}"
export NGROK_AUTH_TOKEN="${NGROK_AUTH_TOKEN:?Set NGROK_AUTH_TOKEN in your shell}"
export PRAISONAI_CALL_PUBLIC_BASE="${PRAISONAI_CALL_PUBLIC_BASE:?Set to the wss:// host Twilio will connect to, e.g. wss://your-app.ngrok.io}"
praisonai call --publicStep 2
Buy a number at PraisonAI Dashboard
Step 3
Enter the Public URL in the PraisonAI Dashboard phone number field
Authentication
The PraisonAI Call server now requires authentication configuration for security. You have two options:
Option 1: Token Authentication (Recommended)
Set a secure token for API authentication:
export CALL_SERVER_TOKEN="${CALL_SERVER_TOKEN:?Set CALL_SERVER_TOKEN in your shell}"
praisonai call --publicAPI requests must include the token in the Authorization header:
curl -H "Authorization: Bearer ${CALL_SERVER_TOKEN:?Set CALL_SERVER_TOKEN in your shell}" \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}' \
http://localhost:8090/api/v1/agents/assistant/invokeTo make retries safe, generate a key once (KEY=$(uuidgen)), send -H "Idempotency-Key: $KEY", and reuse it on every retry — a duplicate returns 409 instead of running the agent again. See n8n API → Safe Retries.
Option 2: Disable Authentication (Local Development Only)
For local development on your own machine, you can disable authentication. The CLI sets the bind-host env var for you, so this is enough when launching via praisonai call:
export PRAISONAI_CALL_AUTH=disabled
praisonai callIf you launch the server programmatically (custom uvicorn, embedding praisonai.api.agent_invoke in another app, or any path that does NOT go through praisonai call / praisonai serve), you must also pin the bind host:
export PRAISONAI_CALL_AUTH=disabled
export PRAISONAI_CALL_BIND_HOST=127.0.0.1Without PRAISONAI_CALL_BIND_HOST set to a localhost value, every request is rejected with:
503 Service Unavailable
PRAISONAI_CALL_AUTH=disabled is only permitted for localhost binding;
set PRAISONAI_CALL_BIND_HOST to 127.0.0.1 when binding locallyAuthentication Flow
When CALL_SERVER_TOKEN is not configured and the environment is not development, the server returns:
503 Service Unavailable
CALL_SERVER_TOKEN is not configured. Set CALL_SERVER_TOKEN or PRAISONAI_CALL_AUTH=disabled to run without authentication.When PRAISONAI_CALL_AUTH=disabled is set but the bind host is not localhost:
503 Service Unavailable
PRAISONAI_CALL_AUTH=disabled is only permitted for localhost binding;
set PRAISONAI_CALL_BIND_HOST to 127.0.0.1 when binding locallyEnvironment Variables
| Env var | Required when | Effect |
|---|---|---|
CALL_SERVER_TOKEN | Production / any non-local exposure | Server requires Authorization: Bearer <token> |
PRAISONAI_CALL_AUTH=disabled | Local development only | Bypasses auth — only honored if PRAISONAI_CALL_BIND_HOST is localhost |
PRAISONAI_CALL_BIND_HOST | Whenever you launch without the praisonai call/praisonai serve CLI | Declares the real bind host so the auth-disabled guard can verify it |
PRAISONAI_CALL_PUBLIC_BASE | Any deployment terminating a Twilio inbound call | ws:///wss:// base URL used to build the outbound /media-stream?session=… URL handed to Twilio. Server returns 503 on /incoming-call if unset. See Public base URL for Twilio media-stream. |
PRAISONAI_CALL_LOAD_DOTENV | Only to restore legacy import-time .env loading | When "true" (case-insensitive), importing praisonai.api.call loads .env from the current working directory at import time. Unset by default — importing the module no longer touches os.environ. The CLI (praisonai call) always loads .env from the CWD at run time regardless of this flag. |
PRAISONAI_ALLOW_LOCAL_TOOLS | Any call-server verb that should honour ./tools.py | Server entry points call _load_local_tools(); unset → the tools registry stays empty even if build_call_app(load_local_tools=True) is passed. See Local Tools Loading. |
Side-effect-free import + build_call_app()
As of PR #4261, import praisonai.api.call is side-effect-free: no FastAPI app is built, no websockets/twilio/uvicorn/pyngrok/rich/fastapi is imported, no ./tools.py filesystem scan runs, and no user code executes. The heavy deps are lazy-imported inside build_call_app(), the route handlers, run_server(), and setup_public_url().
Build the app yourself when embedding the call server in your own process — a build_call_app(*, load_local_tools=False) factory replaces the old top-level app = FastAPI():
# embedding.py
from praisonai.api.call import build_call_app
app = build_call_app(load_local_tools=False) # or True + PRAISONAI_ALLOW_LOCAL_TOOLS=true
# mount under your own FastAPI, or hand to uvicorn.run(app, ...)The load_local_tools flag is only honoured when PRAISONAI_ALLOW_LOCAL_TOOLS=true is also set, matching the CLI opt-in. Each build_call_app() call creates its own CallAppState (hung off app.state.call_state) holding both the tool registry and the pending one-shot session tokens. Two apps in the same process therefore never share tool schemas or cross-consume each other’s stream tokens.
Point the realtime endpoint elsewhere (Azure / self-hosted)
By default the call server connects the Twilio media leg to OpenAI Realtime at wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01 using OPENAI_API_KEY. Teams on Azure OpenAI, OpenRouter, or a self-hosted OpenAI-compatible realtime gateway can override the endpoint without editing the module.
| Env var | Purpose |
|---|---|
PRAISONAI_REALTIME_URL | Full wss://… URL. Takes precedence over the default. |
PRAISONAI_REALTIME_MODEL | Model name for the default OpenAI URL (ignored when PRAISONAI_REALTIME_URL is set). |
PRAISONAI_REALTIME_API_KEY | Bearer key to use for the realtime connection. Falls back to OPENAI_API_KEY when unset. |
# Point voice at Azure OpenAI Realtime
export PRAISONAI_REALTIME_URL="wss://<your-resource>.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=my-realtime-deployment"
export PRAISONAI_REALTIME_API_KEY="${AZURE_OPENAI_KEY:?Set AZURE_OPENAI_KEY in your shell}"
praisonai call --public# Stay on OpenAI but pick a different realtime model
export PRAISONAI_REALTIME_MODEL="gpt-4o-realtime-preview-2024-12-17"
praisonai call --publicWhen PRAISONAI_REALTIME_URL contains openai.com, the server keeps sending the OpenAI-Beta: realtime=v1 header; for any other host, only Authorization: Bearer … is sent.
Connection timeouts
The realtime WebSocket runs with bounded connect / heartbeat / close so a dead upstream cannot hold a Twilio media leg (and phone number) indefinitely.
| Setting | Value |
|---|---|
open_timeout | 10 s |
ping_interval | 20 s |
ping_timeout | 20 s |
close_timeout | 5 s |
max_size (frame cap) | 1 MiB |
Media-stream session tokens
The Twilio path is unchanged — /incoming-call mints a one-shot, 60-second, single-use session token and embeds it as ?session=<token> in the returned TwiML stream URL. /media-stream validates and consumes it once.
Direct WebSocket clients (bots, tests, custom stream consumers) must authenticate with the x-call-token header:
websocat "wss://your-host/media-stream" \
--header "x-call-token: ${CALL_SERVER_TOKEN:?Set CALL_SERVER_TOKEN in your shell}"| Client | Auth path | Notes |
|---|---|---|
Twilio (/incoming-call → /media-stream) | ?session=<one-shot token> minted by /incoming-call | Transparent, single-use, 60 s TTL |
| Direct WebSocket | x-call-token: $CALL_SERVER_TOKEN header | Constant-time compare |
Direct WebSocket via ?token=… | Rejected (4003 Unauthorized) | No longer supported |
All token comparisons (HTTP Authorization, header x-call-token, n8n verify_token, media-stream session tokens) are constant-time (hmac.compare_digest). Connection-count and per-IP rate-limit counters are guarded by asyncio.Locks, so MAX_CONCURRENT_CONNECTIONS and MAX_REQUESTS_PER_WINDOW cannot be over-committed by concurrent WebSocket opens.
Public base URL for Twilio media-stream
/incoming-call builds the outbound wss://…/media-stream?session=<token> URL from the server-side PRAISONAI_CALL_PUBLIC_BASE env var — never from the request Host header. The old behaviour let an authenticated /incoming-call caller redirect Twilio’s live media leg to any host (SSRF / call-audio exfiltration), so the server now refuses to serve /incoming-call when the env var is unset.
# Production — a real wss:// domain
export PRAISONAI_CALL_PUBLIC_BASE="wss://praison.example.com"
praisonai call --host 0.0.0.0
# ngrok dev — copy the wss:// host ngrok prints
export NGROK_AUTH_TOKEN="${NGROK_AUTH_TOKEN:?Set NGROK_AUTH_TOKEN in your shell}"
export PRAISONAI_CALL_PUBLIC_BASE="wss://your-app.ngrok.io"
praisonai call --publicValidation rules — pick a legal value:
- Scheme MUST be
ws://orwss://(nothttp/https). - Host MUST be present (bare
wss://with no host is rejected). - Cleartext
ws://is permitted ONLY for local hosts (localhost,127.0.0.1,::1,[::1]);ws://to a non-local host is rejected because it would expose live audio AND the one-shot session token over an unencrypted transport.
Failure modes:
503 Service Unavailable
PRAISONAI_CALL_PUBLIC_BASE is not configured; refusing to derive the media-stream URL from the client Host header.500 Internal Server Error
PRAISONAI_CALL_PUBLIC_BASE must be a ws:// or wss:// URL500 Internal Server Error
PRAISONAI_CALL_PUBLIC_BASE must include a host500 Internal Server Error
PRAISONAI_CALL_PUBLIC_BASE must use wss:// for non-local hosts; cleartext ws:// would expose live audio and the session tokenBinding & Network Access
The call server binds to 127.0.0.1 by default, so it is only reachable from the same machine.
# Local development (default — same machine only)
praisonai call
# Specific port, still localhost-only
praisonai call --port 8090
# Expose on LAN (warning will be printed)
praisonai call --host 0.0.0.0 --port 8090
# Expose publicly via ngrok
export NGROK_AUTH_TOKEN="${NGROK_AUTH_TOKEN:?Set NGROK_AUTH_TOKEN in your shell}"
praisonai call --publicFeatures
- Make and receive phone calls with AI agents
- Natural language processing for voice interactions
- Support for multiple phone carriers and providers
- Call recording and transcription capabilities
- Integration with other PraisonAI features
Adding Tools
Declare tools = [...] in your tools.py exactly as before — the call server now loads it into each app’s own state.tools registry rather than a module-global list.
- Create a file called
tools.py - Add the following code:
import yfinance as yf
# Get Stock Price definition
get_stock_price_def = {
"name": "get_stock_price",
"description": "Get the current stock price for a given ticker symbol",
"parameters": {
"type": "object",
"properties": {
"ticker_symbol": {
"type": "string",
"description": "The ticker symbol of the stock (e.g., AAPL, GOOGL)"
}
},
"required": ["ticker_symbol"]
}
}
# Get Stock Price function / Tool
async def get_stock_price_handler(ticker_symbol):
try:
stock = yf.Ticker(ticker_symbol)
hist = stock.history(period="1d")
if hist.empty:
return {"error": f"No data found for ticker {ticker_symbol}"}
current_price = hist['Close'].iloc[-1] # Using -1 is safer than 0
return {"price": str(current_price)}
except Exception as e:
return {"error": str(e)}
get_stock_price = (get_stock_price_def, get_stock_price_handler)
tools = [
get_stock_price
]pip install yfinanceexport OPENAI_API_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY in your shell}"
export NGROK_AUTH_TOKEN="${NGROK_AUTH_TOKEN:?Set NGROK_AUTH_TOKEN in your shell}"
praisonai call --publicManage Google Calendar Events
Deploy
Docker Deployment
# Use an official Python runtime as a parent image
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install PraisonAI with the 'call' extra and ensure it's the latest version
RUN pip install --no-cache-dir --upgrade "praisonai[call]"
# Expose the port the app runs on
EXPOSE 8090
# The wss:// host Twilio will connect to — the server returns 503 on /incoming-call if unset.
ENV PRAISONAI_CALL_PUBLIC_BASE=wss://your-public-hostname.example.com
# Bind to all container interfaces — the container boundary is the security boundary.
# Pair with CALL_SERVER_TOKEN for any externally-published port.
CMD ["praisonai", "call", "--host", "0.0.0.0"]How It Works
A caller dials your number, the provider forwards audio to the call server, and the Agent responds with synthesized speech.
Best Practices
Always set CALL_SERVER_TOKEN in production
The server fails with 503 unless a token is set or auth is explicitly disabled for localhost. Never disable auth on a public bind.
Keep the default localhost bind
praisonai call binds to 127.0.0.1 by default. Add --host 0.0.0.0 only inside a container or when you deliberately expose the LAN.
Set secrets via environment variables
Export OPENAI_API_KEY, NGROK_AUTH_TOKEN, and CALL_SERVER_TOKEN in your shell — never inline the raw values.
Use the x-call-token header for direct WebSocket clients
/media-stream no longer accepts ?token= in the query string. Direct callers must pass x-call-token: $CALL_SERVER_TOKEN as a WebSocket handshake header. Twilio flows are unaffected.
Set PRAISONAI_CALL_PUBLIC_BASE to the exact host Twilio will connect to
Never rely on the request Host header — the server refuses to (503). Use wss:// for anything non-local; ws:// is only accepted for localhost/127.0.0.1.
Add tools for real actions
Register async tool handlers (like a stock-price lookup) so the phone agent can fetch live data during a call.