pipedog is an unofficial community port of Pipecat, not affiliated with Daily.
pipedog

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 media events) becomes InputAudioRawFrames at the pipeline's input rate.
  • Bot audio is resampled to 8 kHz and sent as media events.
  • Touch-tone digits become InputDTMFFrames.
  • An InterruptionFrame sends clear.

There is no hang-up REST call: the call ends when the stream closes. See How phone calls connect for the overall flow.

Installation

bun add @pipedog/serializer-exotel @pipedog/transport-websocket @pipedog/runner
import { 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 start message.

ExotelFrameSerializer

Configuration

Prop

Type

InputParams

params is an ExotelFrameSerializer.InputParams (ExotelFrameSerializerInputParams):

ParamTypeDefaultDescription
exotelSampleRatenumber8000Sample rate Exotel uses, in Hz.
sampleRatenumber | nullnullOverride for the pipeline input rate. null uses the pipeline's.
resamplerClearAfterSecsnumber | null0.2Seconds of silence after which the stream resamplers clear their history. null never clears.
ignoreRtviMessagesbooleantrueDrop RTVI messages instead of sending them to Exotel.

Usage

exotel.ts
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

  • callSid is 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

RuntimeWorksWhat it needs
Bun✅ yesNothing.
Node.js✅ yesNothing.
Deno✅ yesNothing.
Cloudflare Workers✅ yesNothing.

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).

On this page