Twilio quickstart
Run a phone bot on Twilio Media Streams from examples/twilio-phone-bot, and test it without a phone.
This guide runs examples/twilio-phone-bot:
call a Twilio number and talk to an assistant built on OpenAI transcription, chat and speech.
Press # to end the call; the bot finishes its sentence and hangs up.
What you need
- Bun to install the repository and run the server (or Deno,
with
bun run start:deno). - An OpenAI API key.
- A Twilio account and a phone number with voice.
- A public HTTPS URL for your machine, e.g. from
ngrok http 8765.
Run it
git clone https://github.com/polvallverdu/pipedog.git
cd pipedog
bun install
cd examples/twilio-phone-bot
OPENAI_API_KEY=sk-... \
TWILIO_ACCOUNT_SID=AC... TWILIO_AUTH_TOKEN=... \
PUBLIC_URL=https://your-tunnel.ngrok.app \
bun run startThen, in the Twilio console, open your phone number and set A call comes in to
Webhook, https://your-tunnel.ngrok.app/twiml, HTTP POST. Call the number.
How it works
The app is one fetch handler with two routes (src/app.ts):
-
POST /twimlis Twilio's voice webhook. It checks theX-Twilio-Signaturewith your auth token and answers with TwiML that connects the call to the media stream, passing the caller's and the dialed numbers as stream parameters:<Response> <Connect> <Stream url="wss://your-tunnel.ngrok.app/ws"> <Parameter name="from_number" value="+15551234567" /> <Parameter name="to_number" value="+15559876543" /> </Stream> </Connect> </Response> -
GET /wsis the Media Streams WebSocket, upgraded bywebsocketFetchHandler. Each connection is one call (src/bot.ts):telephonyHandshakereads Twilio'sconnectedandstartmessages: the stream SID, the call SID and the numbers. A caller who hangs up first ends the session here.TwilioFrameSerializeris built from them, with the account SID and auth token so it can hang up the call.FastAPIWebsocketTransportcarries the call; the pipeline is OpenAI realtime transcription (with turn detection on OpenAI's side),OpenAILLMServiceandOpenAITTSService. The pipeline runs at 16 kHz in and 24 kHz out; the serializer converts to and from Twilio's 8 kHz μ-law.- A small processor ends the pipeline when the caller presses
#; on theEndFrame, the serializerPOSTsStatus=completedto Twilio's REST API.
src/server.ts serves it on Bun (or Node) with serveFetch, which upgrades the media WebSocket
with the runtime's own server; src/deno.ts serves it with Deno.serve. On Cloudflare Workers, serve createPhoneApp(...) from
a Durable Object per call, as in examples/workers-durable-voice-agent.
Test it without a phone
bun test examples/twilio-phone-botThe tests need no API key and no Twilio account. A scripted fake Twilio (test/fake_twilio.ts)
sends connected and start, waits for the greeting, then plays a recorded question as 20 ms
μ-law media events in real time and waits for the answer. The OpenAI APIs are the mock from
examples/test-mocks, and Twilio's REST API is a local stand-in that records the hang-up. The
same call runs in process (through a stand-in for Workers' WebSocketPair), and on Bun and Deno over a real
port.
Next
- How phone calls connect, and the other providers' serializers.
- Twilio serializer reference: every option, regional hosts, and hang-up behavior.
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).
How phone calls connect
How a phone call reaches a pipedog bot over a WebSocket media stream, and which serializer speaks each provider's protocol.
Telephony extensions
IVRNavigator walks a phone menu with DTMF, and VoicemailDetector tells a person from a voicemail on outbound calls. Port of pipecat.extensions.