Exotel
Phone calls over Exotel's Voicebot stream with ExotelFrameSerializer, used with FastAPIWebsocketTransport.
Overview
ExotelFrameSerializer speaks the WebSocket protocol of Exotel's Stream and Voicebot applets.
Paired with FastAPIWebsocketTransport, it lets a bot answer phone
calls:
- Caller audio (16-bit PCM at 8 kHz, base64 in
mediaevents) becomesInputAudioRawFrames at the pipeline's input rate. - Bot audio is resampled to 8 kHz and sent as
mediaevents. - Touch-tone digits become
InputDTMFFrames. - An
InterruptionFramesendsclear.
There is no hang-up REST call: the call ends when the stream closes. See How phone calls connect for the overall flow.
Upstream page
Pipecat's documentation for the same serializer.
Source
packages/serializer-exotel/src/exotel.ts.
Exotel docs
The Stream and Voicebot applets.
Installation
bun add @pipedog/serializer-exotel @pipedog/transport-websocket @pipedog/runnerimport { ExotelFrameSerializer } from "@pipedog/serializer-exotel/exotel";Prerequisites
- An Exotel account with a call flow whose Voicebot applet points at your WebSocket URL.
- The stream SID (and call SID), which Exotel sends in its
startmessage.
ExotelFrameSerializer
Configuration
Prop
Type
InputParams
params is an ExotelFrameSerializer.InputParams (ExotelFrameSerializerInputParams):
| Param | Type | Default | Description |
|---|---|---|---|
exotelSampleRate | number | 8000 | Sample rate Exotel uses, in Hz. |
sampleRate | number | null | null | Override for the pipeline input rate. null uses the pipeline's. |
resamplerClearAfterSecs | number | null | 0.2 | Seconds of silence after which the stream resamplers clear their history. null never clears. |
ignoreRtviMessages | boolean | true | Drop RTVI messages instead of sending them to Exotel. |
Usage
import { parseTelephonyWebsocket } from "@pipedog/runner/utils";
import { ExotelFrameSerializer } from "@pipedog/serializer-exotel/exotel";
import {
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
} from "@pipedog/transport-websocket/fastapi";
import { websocketFetchHandler } from "@pipedog/transport-websocket/fetch";
import type { WebsocketConnection } from "@pipedog/transport-websocket/socket";
async function runCall(connection: WebsocketConnection): Promise<void> {
// Exotel sends "connected", then "start" with the stream and call SIDs.
const [, callData] = await parseTelephonyWebsocket(connection);
const serializer = new ExotelFrameSerializer({
streamSid: callData.streamId ?? "",
callSid: callData.callId,
});
const transport = new FastAPIWebsocketTransport(
connection,
new FastAPIWebsocketParams({
audioInEnabled: true,
audioOutEnabled: true,
addWavHeader: false,
serializer,
}),
);
// ... build and run the pipeline with transport.input() and transport.output()
}
// The Voicebot applet's WebSocket URL points here.
const handler = websocketFetchHandler(runCall);Notes
callSidis stored but not used: Exotel has no hang-up call here.- The audio is linear PCM, not μ-law: only resampled, never companded.
- An invalid DTMF digit is logged at info level and ignored.
Runtime notes
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | Nothing. |
| Node.js | ✅ yes | Nothing. |
| Deno | ✅ yes | Nothing. |
| Cloudflare Workers | ✅ yes | Nothing. |
Generated from pipecat.runtimes and pipecat.compat in packages/serializer-exotel/package.json: ✅ means the compat smoke test passes on that runtime.
Runs on Bun, Node, Deno and Cloudflare Workers. The package's golden tests replay vectors recorded from upstream: at 8 kHz the wire output is byte-identical.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).