apps/runtime is the FastAPI service on port 7860 that answers telephony calls and runs the real-time audio pipeline. One WebSocket connection carries one call. It holds no database of its own — every document it needs comes from the API over REST.
This page covers the service: its routes, how it reaches the API, and what it writes. The turn-by-turn mechanics of the audio loop are in Voice pipeline.
Responsibilities
- Serve
GET|POST /answer?agent_id=&org_id=for telephony agents and return Stream XML, with the provider read fromagent.telephony.provider. - Accept
WS /agent/{org_id}/{agent_id}for both agent categories. - Load agent config and provider credentials from the API on port 8000.
- Build and run a Pipecat pipeline — STT → LLM → TTS — through
apps/providers. - Upload the transcript and recording to MinIO and link them on the CallLog.
Routes
/answer requires both agent_id and org_id as query parameters; either missing returns 400. The webhook body is parsed by decode_webhook_body() from apps/telephony, which accepts JSON or x-www-form-urlencoded, then merged with the query string.
If the parsed event is a hangup, the runtime patches the CallLog with end_time_utc and status: "completed" (plus a mapped call_response where one applies), optionally notifies campaign call status, and returns 200 with no XML. Otherwise it fetches the agent, rejects a non-telephony agent with 400, registers an inbound CallLog when the payload carries a provider call SID, and returns the Stream XML built by build_answer_stream_xml(provider, websocket_url, sample_rate=...).
The WebSocket URL embedded in that XML is derived from VOICE_SERVER_BASE_URL by voice_server_ws_base(), which rewrites https:// to wss:// and http:// to ws://:
Agent modes
agent_category() in services/agent_routing.py defaults an agent with no category to websocket and raises AgentRoutingError on any other value.
- Telephony agents
- WebSocket agents
Answer and hangup URLs are provisioned by the API when the agent is created:The provider —
vobiz or plivo — comes from the agent document. The runtime does not default to a single vendor. On the WebSocket, the first message must be a JSON start event; anything else closes the socket with code 1008. The runtime reads provider_call_sid, streamSid, and the numbers from that event via parse_stream_start(), and registers an inbound CallLog if the answer webhook did not already create one.How it authenticates to the API
The runtime has no user credentials.services/backend.py mints an org-scoped bot JWT and caches it per organisation.
The cached token is reused for 25 minutes (_TOKEN_TTL_SECONDS), a little short of the API’s 30-minute ACCESS_TOKEN_EXPIRE_MINUTES. A 401 forces a refresh and one retry. A missing INTERNAL_API_KEY raises BackendError immediately.
services/ai_service_factory.py then merges the agent’s config.models with the fetched credentials — one GET per kind — and hands the result to apps.providers as an AgentConfig. Any of stt_config, tts_config, or llm_config missing a provider raises ServiceBuildError.
Pipeline modules
services/pipecat/ is nine modules plus three subpackages rather than one function:
services/knowledge/ adds RAG to a call: setup.py, config.py, tool.py, context_processor.py, and formatting.py wire the API’s POST /rag/retrieve into the LLM context. See Knowledge base (RAG).
How these fit together during a live call — frame flow, interruption, VAD, and turn-taking — is covered in Voice pipeline.
Call artifacts
For telephony calls,services/storage/ uploads two objects at call end, keyed by call_id:
transcript.py buffers Pipecat turn messages during the call; object_storage.py writes to MinIO; call_artifacts.py then PATCHes the CallLog with a minio://bucket/key URI on transcript_url or recording_url:
org_id and call_id are sanitised into the object key — any character outside [A-Za-z0-9-_] becomes _. Clients fetch artifacts through the authenticated API proxy, never from MinIO directly:
http://localhost:9001.
Environment
MinIO access is read straight from the environment by
object_storage.py: MINIO_ENDPOINT (default localhost:9000), MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_SECURE, and MINIO_BUCKET (default voicera-calls). RUNTIME_HOST and RUNTIME_PORT control the bind address when the module is run directly.
All variables live in the repository root .env. Inside Compose, API_BASE_URL and MINIO_ENDPOINT are overridden for in-network service discovery — http://api:8000/api/v1 and minio:9000. Full list in Environment variables.
Running it standalone
From the repository root, the whole stack:http://localhost:7860, or RUNTIME_HOST_PORT from .env. Smoke tests:
<Stream …> URL derived from VOICE_SERVER_BASE_URL, for example wss://voice.example.com/agent/YOUR_ORG_ID/YOUR_AGENT_ID. org_id is always passed per request in telephony URLs — there is no global default.
The image is python:3.11-slim with gcc, and pins pipecat-ai[deepgram,cartesia,openai,silero,websocket]==1.8.1. It sets PYTHONPATH=/app and copies apps/runtime, apps/providers, and apps/telephony. Python 3.11 or newer is required — the code relies on StrEnum so that str(Language.EN) == "en" for Deepgram.
Related
- Voice pipeline — inside a live call
- API (apps/api) — the service the runtime authenticates to
- Providers (apps/providers) · Telephony (apps/telephony)
- Calls and call artifacts · Voice and audio troubleshooting