Skip to content

Commit 24b6834

Browse files
authored
Merge pull request #197 from pipecat-ai/twilio-sip-explicit-room-config
Use explicit Daily room config with SIP provider for Twilio bots
2 parents 492905d + 0e13bc5 commit 24b6834

11 files changed

Lines changed: 120 additions & 34 deletions

File tree

phone-chatbot/daily-pstn-cold-transfer/server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ async def handle_incoming_daily_webhook(request: Request) -> JSONResponse:
7979

8080
# Create a Daily room with dial-in capabilities
8181
try:
82-
room_details = await configure(request.app.state.session, sip_caller_phone=caller_phone)
82+
room_details = await configure(
83+
request.app.state.session, sip_caller_phone=caller_phone, enable_dialout=True
84+
)
8385
except Exception as e:
8486
logger.error(f"Error creating Daily room: {e}")
8587
raise HTTPException(status_code=500, detail=f"Failed to create Daily room: {str(e)}")

phone-chatbot/daily-pstn-dial-out/bot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async def attempt_dialout(self) -> bool:
8787
)
8888

8989
# Build dialout settings with phone number and optional caller ID
90-
dialout_params = {"phoneNumber": self._phone_number}
90+
dialout_params = {"phoneNumber": self._phone_number, "displayName": self._phone_number}
9191
if self._caller_id:
9292
dialout_params["callerId"] = self._caller_id
9393
logger.info(f"Using caller ID: {self._caller_id}")

phone-chatbot/daily-pstn-dial-out/server_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ async def create_daily_room(
107107
"""
108108
try:
109109
return await configure(
110-
session, sip_caller_phone=dialout_request.dialout_settings.phone_number
110+
session,
111+
sip_caller_phone="Test",
112+
enable_dialout=True,
111113
)
112114
except Exception as e:
113115
logger.error(f"Error creating Daily room: {e}")

phone-chatbot/daily-pstn-warm-transfer/bot.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,10 @@ async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
190190

191191
# Dial the agent
192192
if self._transfer_target:
193-
dialout_params = {"phoneNumber": self._transfer_target.phone_number}
193+
dialout_params = {
194+
"phoneNumber": self._transfer_target.phone_number,
195+
"displayName": self._transfer_target.phone_number,
196+
}
194197
logger.info(f"Dialing agent: {self._transfer_target.phone_number}")
195198
try:
196199
await self._transport.start_dialout(dialout_params)

phone-chatbot/daily-twilio-sip-dial-in/README.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
# Daily + Twilio SIP dial-in Voice Bot
1+
# Twilio Phone Number → Daily SIP → Pipecat Bot (Dial-in)
22

3-
This project demonstrates how to create a voice bot that can receive phone calls via Twilio and use Daily's SIP capabilities to enable voice conversations.
3+
This example shows how to receive inbound phone calls on a **Twilio phone number** and route them through **Daily's SIP infrastructure** to a **Pipecat voice bot**. The `provider="daily"` setting tells Daily to use its own SIP servers for the media path, so no external SIP trunk is required.
4+
5+
> **Using a Daily phone number instead of Twilio?** See the [`daily-pstn-dial-in`](../daily-pstn-dial-in) example.
46
57
## How It Works
68

7-
1. Twilio receives an incoming call to your phone number
8-
2. Twilio calls your webhook server (`/call` endpoint in `server.py`)
9-
3. The server creates a Daily room with SIP capabilities
10-
4. The server starts the bot process with the room details (locally or via Pipecat Cloud)
11-
5. The caller is put on hold with music (a US ringtone in this example)
12-
6. The bot joins the Daily room and signals readiness
13-
7. Twilio forwards the call to Daily's SIP endpoint
14-
8. The caller and the bot are connected, and the bot handles the conversation
9+
```
10+
Caller → Twilio Phone Number → Webhook (server.py) → Daily SIP Room → Pipecat Bot
11+
```
12+
13+
1. A caller dials your **Twilio phone number**
14+
2. Twilio sends a webhook to your server (`/call` endpoint in `server.py`)
15+
3. The caller hears hold music while the server handles the webhook
16+
4. The server creates a **Daily room** with SIP enabled (`provider="daily"`)
17+
5. The server starts the Pipecat bot (locally or via Pipecat Cloud)
18+
6. The bot joins the Daily room and signals readiness (`dialin-ready` fires)
19+
7. TwiML is invoked that asks Twilio to forward the call to Daily's SIP endpoint
20+
8. The caller and the bot are connected for a voice conversation
1521

1622
## Project Structure
1723

@@ -175,6 +181,7 @@ class AgentRequest(BaseModel):
175181
token: str
176182
call_sid: str
177183
sip_uri: str
184+
to_phone: str
178185
# Add your custom fields here
179186
customer_name: str | None = None
180187
account_id: str | None = None
@@ -191,6 +198,7 @@ agent_request = AgentRequest(
191198
token=sip_config.token,
192199
call_sid=call_data.call_sid,
193200
sip_uri=sip_config.sip_endpoint,
201+
to_phone=call_data.to_phone,
194202
customer_name=customer_info.name,
195203
account_id=customer_info.id,
196204
)

phone-chatbot/daily-twilio-sip-dial-in/bot.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,16 @@ async def on_dialin_ready(transport, sip_endpoint):
9898
logger.info(f"Forwarding call {request.call_sid} to {request.sip_uri}")
9999

100100
try:
101-
twilio_client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
101+
# Use to_phone to select Twilio credentials if you have multiple
102+
# accounts (e.g., US vs EU). For now, we use a single set of credentials.
103+
to_phone = request.to_phone
104+
logger.info(f"Dialing in to {to_phone}")
105+
106+
account_sid = os.getenv("TWILIO_ACCOUNT_SID")
107+
auth_token = os.getenv("TWILIO_AUTH_TOKEN")
108+
logger.info(f"Using Twilio credentials for {to_phone}")
109+
110+
twilio_client = Client(account_sid, auth_token)
102111

103112
# Update the Twilio call with TwiML to forward to the Daily SIP endpoint
104113
twilio_client.calls(request.call_sid).update(
@@ -114,6 +123,12 @@ async def on_dialin_error(transport, data):
114123
logger.error(f"Dial-in error: {data}")
115124
await task.cancel()
116125

126+
@transport.event_handler("on_dtmf_event")
127+
async def on_dtmf_event(transport, data):
128+
logger.info(f"DTMF event: {data}")
129+
# Echo back the DTMF tone to the caller
130+
# await transport.send_dtmf({"tones": data["tone"], "duration": 100})
131+
117132
@transport.event_handler("on_client_connected")
118133
async def on_client_connected(transport, client):
119134
logger.info("Client connected")

phone-chatbot/daily-twilio-sip-dial-in/server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ async def handle_call(request: Request):
7878

7979
call_data = await twilio_call_data_from_request(request)
8080

81-
sip_config = await create_daily_room(call_data, request.app.state.http_session)
81+
sip_config = await create_daily_room(
82+
call_data, request.app.state.http_session, sip_provider="daily"
83+
)
8284

8385
# Make sure we have a SIP endpoint.
8486
if not sip_config.sip_endpoint:
@@ -89,6 +91,7 @@ async def handle_call(request: Request):
8991
token=sip_config.token,
9092
call_sid=call_data.call_sid,
9193
sip_uri=sip_config.sip_endpoint,
94+
to_phone=call_data.to_phone,
9295
)
9396

9497
# Start bot locally or in production.

phone-chatbot/daily-twilio-sip-dial-in/server_utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class AgentRequest(BaseModel):
4141
token: str
4242
call_sid: str
4343
sip_uri: str
44+
to_phone: str
4445

4546

4647
async def twilio_call_data_from_request(request: Request):
@@ -67,13 +68,16 @@ async def twilio_call_data_from_request(request: Request):
6768

6869

6970
async def create_daily_room(
70-
call_data: TwilioCallData, session: aiohttp.ClientSession
71+
call_data: TwilioCallData,
72+
session: aiohttp.ClientSession,
73+
sip_provider: str | None = None,
7174
) -> DailyRoomConfig:
7275
"""Create a Daily room configured for PSTN dial-in.
7376
7477
Args:
7578
call_data: Call data containing caller phone number and call details
7679
session: Shared aiohttp session for making HTTP requests
80+
sip_provider: Optional SIP provider name (e.g., "daily")
7781
7882
Returns:
7983
DailyRoomConfig: Configuration object with room_url and token
@@ -82,7 +86,13 @@ async def create_daily_room(
8286
HTTPException: If room creation fails
8387
"""
8488
try:
85-
return await configure(session, sip_caller_phone=call_data.from_phone)
89+
return await configure(
90+
session,
91+
sip_caller_phone=call_data.from_phone,
92+
sip_provider=sip_provider,
93+
enable_dialout=True,
94+
room_geo="us-east-1", # can set this to the same region as your Twilio number
95+
)
8696
except Exception as e:
8797
logger.error(f"Error creating Daily room: {e}")
8898
raise HTTPException(status_code=500, detail=f"Failed to create Daily room: {e!s}")

phone-chatbot/daily-twilio-sip-dial-out/README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
# Daily + Twilio SIP dial-out Voice Bot
1+
# Pipecat Bot → Daily SIP → Twilio Phone Number (Dial-out)
22

3-
This project demonstrates how to create a voice bot that uses Daily's SIP capabilities with Twilio to make outbound calls to phone numbers.
3+
This example shows how a **Pipecat voice bot** can make outbound phone calls through **Daily's SIP infrastructure** to a **Twilio phone number**. The `provider="daily"` setting in the dial-out request tells Daily to use its own SIP servers for the media path.
4+
5+
> **Using a Daily phone number instead of Twilio?** See the [`daily-pstn-dial-out`](../daily-pstn-dial-out) example — no Twilio SIP domain/TwiML configuration needed. However, Twilio has phone numbers in multiple regions.
46
57
## How It Works
68

7-
1. The server receives a dial-out request with the SIP URI to call
8-
2. The server creates a Daily room with SIP capabilities
9-
3. The server starts the bot process (locally or via Pipecat Cloud based on ENV)
10-
4. The bot joins the room and initiates the dial-out to the specified SIP URI
11-
5. Twilio receives the SIP request and processes it via configured TwiML
12-
6. Twilio rings the number found within the SIP URI
9+
```
10+
API Request (curl) → server.py → Spins up Pipecat Bot → Daily SIP dial-out → Twilio SIP Domain → Phone
11+
```
12+
13+
1. Your server receives a dial-out request with the SIP URI and `provider`
14+
2. The server creates a **Daily room** with dial-out enabled
15+
3. The server starts the Pipecat bot (locally or via Pipecat Cloud)
16+
4. The bot joins the room and initiates the dial-out (`provider="daily"`)
17+
5. **Daily's SIP** sends the call to your **Twilio SIP domain**
18+
6. Twilio processes the call via your TwiML bin or webhook handler and rings the destination number
1319
7. The bot automatically retries on failure (up to 5 attempts)
1420
8. When the call is answered, the bot conducts the conversation
1521

@@ -95,6 +101,8 @@ This example is organized to be production-ready and easy to customize:
95101
- callerId must be a valid number that you own on [Twilio](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
96102
- answerOnBridge="true|false" based on your use-case
97103
- Save the file. We will use this when creating the SIP domain
104+
105+
note: `callerid` is hardcoded in the TwiML bin, to set it dynamically, the username field of the SIP URI can be overloaded to contain both the callerId and the destination number. `+1DESTINATION_+1CALLERID`.
98106

99107
4. Create and configure a SIP domain
100108

@@ -171,7 +179,8 @@ You'll need two terminal windows open:
171179
-H "Content-Type: application/json" \
172180
-d '{
173181
"dialout_settings": {
174-
"sip_uri": "sip:+1234567890@daily.sip.twilio.com"
182+
"sip_uri": "sip:+1234567890@daily.sip.twilio.com",
183+
"provider": "daily"
175184
}
176185
}'
177186
```
@@ -269,3 +278,5 @@ agent_request = AgentRequest(
269278
- Make sure both IP ACLs (0.0.0.0/1 and 128.0.0.0/1) are created and selected
270279
- Verify that the TwiML bin has a valid caller ID from your Twilio account
271280
- Check that the SIP domain name matches what you're using in the SIP URI
281+
282+
Note: Setting Daily Provider come with the advantage of using Static IPs, which means you can set a smaller set of IP in the ACLs and have reliable SIP connectivity.

phone-chatbot/daily-twilio-sip-dial-out/bot.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(
5151
):
5252
self._transport = transport
5353
self._sip_uri = dialout_settings.sip_uri
54+
self._provider = dialout_settings.provider
5455
self._max_retries = max_retries
5556
self._attempt_count = 0
5657
self._is_successful = False
@@ -79,8 +80,16 @@ async def attempt_dialout(self) -> bool:
7980
logger.info(
8081
f"Attempting dialout (attempt {self._attempt_count}/{self._max_retries}) to: {self._sip_uri}"
8182
)
82-
83-
await self._transport.start_dialout({"sipUri": self._sip_uri})
83+
sip_uri = self._sip_uri
84+
display_name = sip_uri # fallback
85+
if sip_uri.startswith("sip:") and "@" in sip_uri:
86+
phone_part = sip_uri[4:] # Remove 'sip:' prefix
87+
display_name = phone_part.split("@")[0] # Get everything before '@'
88+
89+
params = {"sipUri": sip_uri, "displayName": display_name}
90+
if self._provider:
91+
params["provider"] = self._provider
92+
await self._transport.start_dialout(params)
8493
return True
8594

8695
def mark_successful(self):
@@ -169,6 +178,25 @@ async def on_dialout_answered(transport, data):
169178
logger.debug(f"Dial-out answered: {data}")
170179
dialout_manager.mark_successful()
171180

181+
@transport.event_handler("on_dialout_connected")
182+
async def on_dialout_connected(transport, data):
183+
logger.debug(f"Dial-out connected: {data}")
184+
185+
@transport.event_handler("on_dialout_stopped")
186+
async def on_dialout_stopped(transport, data):
187+
logger.debug(f"Dial-out stopped: {data}")
188+
await task.cancel()
189+
190+
@transport.event_handler("on_dialout_warning")
191+
async def on_dialout_warning(transport, data):
192+
logger.debug(f"Dial-out warning: {data}")
193+
194+
@transport.event_handler("on_dtmf_event")
195+
async def on_dtmf_event(transport, data):
196+
logger.info(f"DTMF event: {data}")
197+
# Echo back the DTMF tone to the caller
198+
# await transport.send_dtmf({"tones": data["tone"], "duration": 100})
199+
172200
@transport.event_handler("on_dialout_error")
173201
async def on_dialout_error(transport, data: Any):
174202
logger.error(f"Dial-out error, retrying: {data}")

0 commit comments

Comments
 (0)