Skip to main content
Turn an Agent into a phone assistant — start the call server and connect a phone number to talk to it over the line.
When --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 Dashboard

Step 3

Enter the Public URL in the PraisonAI Dashboard phone number field

Authentication

Breaking Change: When upgrading from earlier versions, the call server requires authentication configuration or it will fail with a 503 error.
The PraisonAI Call server now requires authentication configuration for security. You have two options: Set a secure token for API authentication:
API requests must include the token in the Authorization header:

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:
If 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:
Without PRAISONAI_CALL_BIND_HOST set to a localhost value, every request is rejected with:
Never use PRAISONAI_CALL_AUTH=disabled in production. It is rejected outright unless the server is bound to localhost, and even then it bypasses all caller verification.

Authentication Flow

When CALL_SERVER_TOKEN is not configured and the environment is not development, the server returns:
When 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():
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.
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 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.
When 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

Breaking change (this release): /media-stream no longer accepts the shared CALL_SERVER_TOKEN in the URL query string. Direct WebSocket clients that previously connected with ?token=$CALL_SERVER_TOKEN are now rejected with WebSocket close code 4003 Unauthorized.
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.
Direct WebSocket clients (bots, tests, custom stream consumers) must authenticate with the 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.
Breaking change (Twilio operators): anyone who relied on the previous “host from the Host header” behaviour must now export PRAISONAI_CALL_PUBLIC_BASE. praisonai call --public (ngrok) users export PRAISONAI_CALL_PUBLIC_BASE=wss://<the-ngrok-host> before dialling in. The old behaviour is not restorable via a flag — it was the SSRF surface being closed.
Validation rules — pick a legal value:
  • Scheme MUST be ws:// or wss:// (not http/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:

Binding & Network Access

The call server binds to 127.0.0.1 by default, so it is only reachable from the same machine.
Breaking change (earlier versions → this release): Earlier versions of praisonai call bound to 0.0.0.0 unconditionally, exposing the server to your entire LAN. Starting in this release, the default is 127.0.0.1. Production deployments that previously relied on the LAN-exposed default must add --host 0.0.0.0 (or a specific NIC) and ensure CALL_SERVER_TOKEN is set.

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

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.
  1. Create a file called tools.py
  2. Add the following code:
pip install yfinance

Manage Google Calendar Events

See Google Calendar Tools

Deploy

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

The server fails with 503 unless a token is set or auth is explicitly disabled for localhost. Never disable auth on a public 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.
Export OPENAI_API_KEY, NGROK_AUTH_TOKEN, and CALL_SERVER_TOKEN in your shell — never inline the raw values.
/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.
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.
Register async tool handlers (like a stock-price lookup) so the phone agent can fetch live data during a call.

Tools

Give your call agent live data and actions.

Security

Harden the call server before exposing it.