Overview
A telephony provider is implemented as a self-registering package underapi/services/telephony/providers/<name>/. The package contributes everything Ambivert AI needs to wire the provider in — the provider class, transport factory, audio config, request/response schemas, optional HTTP routes, and the form metadata used to render its configuration UI — through a single ProviderSpec registered at import time.
Adding a new provider should not require touching the factory, the audio config, the API routes module, the run-pipeline module, or the frontend. The only edits outside the provider folder are:
- One import line in
api/services/telephony/providers/__init__.py - One import line in
api/schemas/telephony_config.pyto add the request/response classes to theTelephonyConfigRequestdiscriminated union
Provider Package Layout
__init__.py, config.py, provider.py, transport.py). The rest are optional and are discovered automatically when present:
routes.py— if the module exists and exportsrouter: APIRouter, the routes module is imported lazily and mounted under/api/v1/telephonybyapi.routes.telephonyviaimportlib. Providers that only stream over WebSocket (e.g. ARI) can omit it.strategies.py— used by transports that need provider-specific call transfer/hangup logic in the frame serializer (e.g. Twilio Conference transfers).serializers.py— typically a re-export from pipecat. Keep the file even when it’s a one-line re-export so transport code imports from.serializers, giving you an obvious place to drop a custom subclass later.
The TelephonyProvider Interface
Subclass TelephonyProvider in provider.py:
api/services/telephony/base.py for the full docstrings on each method.
Implementation Guide
1. Configuration schemas
Define Pydantic models for the credential payload. Theprovider Literal discriminator is what makes the schemas dispatch correctly through the registry’s discriminated union.
2. Transport factory
Build the PipecatFastAPIWebsocketTransport for accepted WebSockets. Always load credentials through load_credentials_for_transport so the right config row is picked when the workflow run carries a telephony_configuration_id (multi-config orgs).
3. Routes (optional)
If your provider POSTs webhooks to Ambivert AI (answer URL, status callbacks, hangup callbacks), expose them through a module-levelrouter. The routes are auto-mounted under /api/v1/telephony.
importlib from api.routes.telephony._mount_provider_routers, so your route module can freely import other backend services without creating import cycles at provider-class load time.
4. Register the ProviderSpec
The package’s __init__.py is where everything comes together:
ProviderSpec covers everything downstream code needs:
5. Wire the package into the registry import chain
Add one import line toapi/services/telephony/providers/__init__.py:
6. Add to the discriminated union
Add one import block toapi/schemas/telephony_config.py so the request/response classes participate in the TelephonyConfigRequest union and the TelephonyConfigurationResponse shape:
Frontend
The configuration form is metadata-driven. The UI callsGET /api/v1/organizations/telephony-providers/metadata, gets back the list of providers and their ProviderUIField definitions, and renders each form generically. No per-provider frontend code is needed — your ProviderUIMetadata declaration is what drives the form.
If you add a new field type that the existing renderer doesn’t support (e.g. a file upload), extend the renderer in ui/src/app/(authenticated)/telephony-configurations/. The supported ProviderUIField.type values today are text, password, textarea, string-array, and number.
Audio Format Considerations
Each provider declares its wire format through itsAudioConfig. Common shapes:
- Twilio / Plivo: 8 kHz μ-law, base64-encoded JSON frames
- Vonage: 16 kHz Linear PCM as binary frames
- Asterisk ARI: 8 kHz Linear PCM via externalMedia
Testing
Best Practices
- Trust the registry — never import another provider’s class directly; resolve through the factory helpers (
get_default_telephony_provider,get_telephony_provider_by_id, etc.). - Sensitive fields — mark every credential field
sensitive=TrueinProviderUIMetadata. The save endpoint masks these on read and preserves the original when the client re-submits a masked value. - Inbound signature verification — always validate inbound webhook signatures in
verify_inbound_signature. ReturningTruewhen no signature header is present is acceptable; returnFalsewhen a signature is present but invalid. - Transports load credentials lazily — call
load_credentials_for_transportwith thetelephony_configuration_idfrom the workflow run. Don’t read the org’s default config fromtransport.py. - Logging — use
loguru.logger.
Reference Implementations
Use ARI as the smallest viable example when your provider doesn’t expose HTTP
webhooks, and Twilio as the reference when it does.