Skip to main content
apps/telephony holds provider-agnostic HTTP clients and helpers for Vobiz and Plivo. Like apps/providers it is a library, not a container: it is copied into both the api and runtime images. The API uses it to provision applications and phone numbers; the runtime uses it to build answer XML, parse webhooks, and serialize audio frames.
This page covers the package layout and public API. The conceptual model — applications, numbers, inbound versus outbound, what each provider owns — is in Telephony model.
Credentials and base_url are always injected by the caller. The package never reads ProviderAuth, FerretDB, or environment variables for auth.

Public API

The catalog follows the same pattern as apps.providers.schema:
apps/telephony/__init__.py calls load_providers() at import, so vendor configs are registered as soon as the package is imported. Frame serializers are the exception — they need Pipecat and are loaded separately, on demand. Dump the catalog from a shell:

Method map

Application and outbound methods return {status, message, ...} dicts built by the ApiResult dataclass in base.py, where status is "success" or "fail". Recording helpers return ids or bytes, or None. list_recordings_for_call exists on Plivo only.

Outbound call

The caller resolves credentials and builds the answer and hangup URLs; the library only makes the HTTP request:
initiate_outbound() builds the typed config, creates the client, and calls initiate_call(). The Vobiz payload carries no hangup fields; Plivo includes hangup_url and hangup_method when they are provided.

Answer XML

The runtime keeps a single /answer route and dispatches on the agent’s provider:
sample_rate defaults to 8000 and selects the contentType. The XML format is identical for Vobiz and Plivo today, but each provider owns its own xml.py so the two can diverge later. Callers use the parent dispatcher only.

Per-provider layout

Every provider under providers/<name>/ follows the same contract: registry.py keeps four maps — TELEPHONY_CONFIGS, CLIENT_CREATORS, ANSWER_XML_BUILDERS, and FRAME_SERIALIZER_FACTORIES — and rejects a duplicate registration for a provider id with a ValueError. Provider ids are normalised to lower case, so lookups are case-insensitive. Do not add provider if/elif chains to the package-root xml.py, calls.py, or serializers.py. Those three modules are thin dispatchers over the registry, and that is all they should be. To add a vendor, follow Adding a telephony provider.

Frame serializers

Serializers require Pipecat, so they live behind a second, lazy discovery pass and are never re-exported from apps.telephony — the API can use Application and Recording without Pipecat installed. The runtime imports the parent factory:
An unregistered provider raises ValueError. Per-provider classes are importable directly when you need the type:

Webhooks

webhooks.py normalises what the two providers send, so the runtime’s /answer route never branches on vendor field names. Missing numbers default to "unknown" rather than raising, and direction defaults to "inbound".

Schemas

schema.py mirrors apps.providers.schema. provider_schemas(Kind.TELEPHONY) returns a readable catalog per provider with secrets, required, and fields; configuration_telephony() wraps it with default_providers, where the telephony default is vobiz. Field extras carried through to the catalog are secret, examples, multiline, docs_url, and integration_model. kind, provider, and name are omitted from fields. This is what GET /api/v1/configuration/telephony returns.

Out of scope

The package deliberately does not do these things. Its callers do.
  • Phone attach and detach against FerretDB — that is apps/api.
  • MinIO storage and recording submission — that is apps/runtime.
  • The FastAPI /answer route and the WebSocket endpoint — the runtime owns those and calls the XML helper.
  • Agent config and credential lookup — the caller injects auth_id, auth_token, and base_url.
apps/telephony/README.md in the repository is the package’s own reference, kept in step with this page.