--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
Step 2
Buy a number at PraisonAI DashboardStep 3
Enter the Public URL in the PraisonAI Dashboard phone number fieldAuthentication
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: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 viapraisonai call:
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:
PRAISONAI_CALL_BIND_HOST set to a localhost value, every request is rejected with:
Authentication Flow
WhenCALL_SERVER_TOKEN is not configured and the environment is not development, the server returns:
PRAISONAI_CALL_AUTH=disabled is set but the bind host is not localhost:
Environment Variables
Importing
praisonai.api.call no longer loads .env at import time (was a silent side effect). The praisonai call CLI still loads it explicitly at run time — user-facing behaviour is unchanged. To restore the legacy import-time behaviour (e.g. in a wrapper module that imports the call server but bypasses main()), set PRAISONAI_CALL_LOAD_DOTENV=true.The token is read fresh on every request.
CALL_SERVER_TOKEN no longer has to be set before import praisonai. A late-loaded .env (loaded by a wrapper after import) and runtime token rotation (re-export the env var without restarting) both work — every authenticated request calls os.getenv('CALL_SERVER_TOKEN') again. This holds for every host that mounts praisonai.api.agent_invoke.router, not just the praisonai call CLI.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():
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.
Co-hosting multiple call apps in one process is now safe:
state.tools and state.pending_sessions are scoped to the app, not module-global. praisonai.api.call.app (the lazy singleton) still exists for legacy imports and keeps its own state, isolated from any explicitly built app.praisonai.api.call.app still resolves — a module-level __getattr__ lazily builds it on first attribute access, so third-party code that imports the name directly keeps working. Touching .app builds a FastAPI app (and pulls its transitive deps); code that only wants import_tools_from_file should not touch .app.Point the realtime endpoint elsewhere (Azure / self-hosted)
By default the call server connects the Twilio media leg to OpenAI Realtime atwss://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.
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.Introduced in v4.6.163 (PraisonAI PR #3879). Prior to this release, a stuck realtime connection kept the Twilio call live until the carrier hung up — which continues to be billed. These are hardcoded, not env-tunable; open an issue if you need a knob.
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.
Effect for Twilio operators: nothing to configure — the change is transparent for
praisonai call / praisonai call --public. Custom integrations that hard-coded the token in the media-stream URL must move it to the x-call-token header.x-call-token header:
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.
- 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.
Binding & Network Access
The call server binds to127.0.0.1 by default, so it is only reachable from the same machine.
Features
- 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
Declaretools = [...] 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:
Manage Google Calendar Events
See Google Calendar ToolsDeploy
Docker Deployment
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
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
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
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
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
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
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.
Related
Tools
Give your call agent live data and actions.
Security
Harden the call server before exposing it.

