> ## Documentation Index
> Fetch the complete documentation index at: https://daily-docs-source-analytics-user-turn-strategies.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Development Runner

> Unified runner for building voice AI bots with Daily, WebRTC, and telephony transports

## Overview

The Pipecat development runner provides a unified way to run voice AI bots across multiple transport types. It handles infrastructure setup - creating Daily rooms, managing WebRTC connections, and routing telephony calls.

## Installation

```bash theme={null}
uv add "pipecat-ai[runner]"
```

## What is a Runner?

A runner in Pipecat refers to a "bot runner", an HTTP service that provides a gateway for spawning bots on-demand. It's the component that enables your bot to run by providing it with server infrastructure and connection details like rooms and tokens.

A bot runner typically creates transport sessions (like Daily WebRTC rooms), generates authentication tokens for both bots and users, spawns new bot processes when users request sessions, and manages bot lifecycle and cleanup. Think of it as the bridge between incoming user connections and your bot logic.

## How the Development Runner Works

The development runner operates as a FastAPI web server that automatically discovers and executes your bot code. By default it starts a **single server that supports all transports simultaneously** — WebRTC, Daily, telephony (Twilio, Telnyx, Plivo, Exotel), and plain WebSocket. Clients choose which transport they want per-request by sending a `"transport"` field in the `POST /start` body; you no longer restart the server to switch transports.

* **WebRTC connections**: It serves a built-in web interface where users can connect directly as well as an endpoint to create new WebRTC sessions
* **Daily integration**: It provides endpoints that create new rooms and tokens and redirect users to join them
* **Telephony providers**: For Twilio, Telnyx, and Plivo, it sets up webhook endpoints that handle incoming calls and establish WebSocket connections for audio streaming
* **Plain WebSocket**: It exposes a `/ws-client` endpoint for non-telephony WebSocket clients (e.g. browser apps using protobuf framing)

The runner registers the infrastructure for every transport and dispatches each connection to the right handler. It then discovers your bot function and spawns new instances whenever users connect. This means you can focus on writing your bot logic while the runner handles all the server infrastructure, connection management, and transport-specific details.

<Tip>
  Passing `-t/--transport` is optional. Omit it to support all transports at
  once (the default). Pass it (e.g. `-t daily`) to **restrict** the server to a
  single transport and make it the default for `/start`.
</Tip>

Your bot code receives runner arguments that contain everything it needs, including Daily room URLs and tokens, WebRTC connections, or WebSocket streams for telephony. The runner abstracts away the complexity of managing these different connection types, providing a unified interface for building bots that work across multiple platforms.

## Pipecat Cloud Ready

The bot runner is designed to be cloud-ready, meaning that you can run the same bot code locally and deployed to Pipecat Cloud without any modifications. It automatically handles the differences in transport setup, providing you with the flexibility to test locally using an open source transport, like SmallWebRTCTransport, but run in production using Daily or telephony transports.

## Building with the Runner

Now let's build a practical example to see how this works. The key insight is that your bot code is structured into two parts: the core bot logic that works with any transport, and the entry point that creates the appropriate transport based on the runner arguments.

Here's the basic structure:

```python theme={null}
# Your imports
from pipecat.runner.types import RunnerArguments
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.network.small_webrtc import SmallWebRTCTransport

async def run_bot(transport: BaseTransport):
    """Your core bot logic here:
    - Define services (STT, TTS, LLM)
    - Initialize messages and context
    - Create the pipeline
    - Add event handlers
    - Run the pipeline
    """

    # Your bot logic goes here
    # Define STT, LLM, TTS...
    # ...
    # Run your pipeline
    await runner.run(worker)

async def bot(runner_args: RunnerArguments):
    """Main bot entry point compatible for local dev and Pipecat Cloud."""
    transport = SmallWebRTCTransport(
        params=TransportParams(
            audio_in_enabled=True,
            audio_out_enabled=True,
        ),
        webrtc_connection=runner_args.webrtc_connection,
    )

    await run_bot(transport)

if __name__ == "__main__":
    from pipecat.runner.run import main
    main()
```

The `run_bot()` function contains your actual bot logic and is transport-agnostic. The `bot()` function is the entry point that the runner calls - it creates the appropriate transport and passes it to your bot logic. This separation allows the same bot code to work across different transports.

When you run this with `python bot.py`, the development runner starts a web server and opens a browser interface at `http://localhost:7860/client`. Each time someone connects, the runner calls your `bot()` function with WebRTC runner arguments.

<Warning>
  `webrtc_connection` only exists on `SmallWebRTCRunnerArguments`, not on the
  base `RunnerArguments` class. The example above works because it runs with the
  WebRTC transport. If you run with any other transport, accessing
  `runner_args.webrtc_connection` raises `AttributeError: 'RunnerArguments'
      object has no attribute 'webrtc_connection'`. To support more than one
  transport, branch on the argument type with the `isinstance` pattern shown in
  [Supporting Multiple Transports](#supporting-multiple-transports) below.
</Warning>

## Supporting Multiple Transports

To make your bot work across different platforms, you can detect the transport type from the runner arguments and create the appropriate transport. Here's how to support both Daily and WebRTC:

```python theme={null}
from pipecat.runner.types import DailyRunnerArguments, RunnerArguments, SmallWebRTCRunnerArguments

async def bot(runner_args: RunnerArguments):
    """Main bot entry point compatible with Pipecat Cloud."""

    transport = None

    if isinstance(runner_args, DailyRunnerArguments):
        from pipecat.transports.daily.transport import DailyParams, DailyTransport

        transport = DailyTransport(
            runner_args.room_url,
            runner_args.token,
            "Pipecat Bot",
            params=DailyParams(
                audio_in_enabled=True,
                audio_out_enabled=True,
            ),
        )

    elif isinstance(runner_args, SmallWebRTCRunnerArguments):
        from pipecat.transports.base_transport import TransportParams
        from pipecat.transports.network.small_webrtc import SmallWebRTCTransport

        transport = SmallWebRTCTransport(
            params=TransportParams(
                audio_in_enabled=True,
                audio_out_enabled=True,
            ),
            webrtc_connection=runner_args.webrtc_connection,
        )

    else:
        logger.error(f"Unsupported runner arguments type: {type(runner_args)}")
        return

    if transport is None:
        logger.error("Failed to create transport")
        return

    await run_bot(transport)

if __name__ == "__main__":
    from pipecat.runner.run import main
    main()
```

Now you can run your bot. A single server supports every transport at once, and the client picks which one to use when it starts a session:

```bash theme={null}
python bot.py            # All transports; client selects via the /start request
```

If you want to limit the server to one transport (and make it the default for `/start`), pass `-t/--transport`:

```bash theme={null}
python bot.py -t webrtc  # Restrict to WebRTC → SmallWebRTCRunnerArguments
python bot.py -t daily   # Restrict to Daily → DailyRunnerArguments
```

### Understanding Runner Arguments

Runner arguments are how the runner communicates transport-specific information to your bot. The runner determines which transport to use based on the command-line arguments, then creates the appropriate runner arguments:

* **`DailyRunnerArguments`**: Contains `room_url`, `token` (Optional), `body` (Optional) for joining Daily rooms
* **`SmallWebRTCRunnerArguments`**: Contains `webrtc_connection` for local WebRTC sessions, `body` (Optional) for WhatsApp call metadata
* **`WebSocketRunnerArguments`**: Contains `websocket` for both telephony and plain WebSocket connections. The `transport_type` field distinguishes them — `"websocket"` for plain (non-telephony) clients connecting via `/ws-client`, or a telephony provider value for connections via `/ws`.

All runner arguments also include:

* **`handle_sigint`**: Whether the bot should handle SIGINT (Ctrl+C) signals (managed automatically)
* **`handle_sigterm`**: Whether the bot should handle SIGTERM signals (managed automatically)

<Note>
  For WhatsApp connections, `SmallWebRTCRunnerArguments.body` contains a
  `WhatsAppConnectCall` object with caller metadata (phone number, call ID,
  direction, timestamp). This allows your bot to access caller information
  without additional API calls.
</Note>

<Warning>
  `handle_sigint` and `handle_sigterm` are development only features and cannot
  be used when deploying to Pipecat Cloud.
</Warning>

The runner handles all the complex setup - creating Daily rooms, generating tokens, establishing WebSocket connections - and provides your bot with everything it needs through these runner arguments.

Notice how we use lazy imports (`from pipecat.transports.daily.transport import ...`) inside the conditional blocks. This ensures that transport-specific dependencies are only required when that transport is actually used, making your bot more portable.

<Info>
  `RunnerArguments` is the base class for all runner arguments. It provides a
  common interface for the runner to pass transport-specific information to your
  bot.
</Info>

## Environment Detection

When building bots that work both locally and in production, you often need to detect the execution environment to enable different features. The development runner sets the `ENV` environment variable to help with this:

```python theme={null}
import os

async def bot(runner_args: RunnerArguments):
    # Check if running in local development environment
    is_local = os.environ.get("ENV") == "local"

    # Enable production features only when deployed
    if not is_local:
        from pipecat.audio.filters.krisp_viva_filter import KrispVivaFilter
        krisp_filter = KrispVivaFilter()
    else:
        krisp_filter = None

    transport = DailyTransport(
        runner_args.room_url,
        runner_args.token,
        "Pipecat Bot",
        params=DailyParams(
            audio_in_filter=krisp_filter,  # Krisp VIVA filter only in production
            audio_in_enabled=True,
            audio_out_enabled=True,
        ),
    )
```

### Environment Values

The development runner automatically sets environment variables based on how your bot is running:

* **Local development**: `ENV=local` (set by the development runner)
* **Production/Cloud deployment**: `ENV` is not set or has a different value

This allows you to easily customize behavior between development and production environments:

## All Supported Transports

The development runner supports seven transport types, each designed for different use cases. By default all are available from one server; the client selects one per session via the `"transport"` field in the `/start` request. The `-t <transport>` form below is the optional way to **restrict** the server to a single transport.

### WebRTC (`"transport": "webrtc"`)

Local WebRTC connections with a built-in browser interface. Perfect for development and testing. This is the default transport when a `/start` request omits the `"transport"` field.

```bash theme={null}
python bot.py
# Prebuilt UI at http://localhost:7860/client

# Restrict to WebRTC only
python bot.py -t webrtc

# ESP32 compatibility mode
python bot.py --esp32 --host 192.168.1.100
```

**Runner Arguments**: `SmallWebRTCRunnerArguments`

* `webrtc_connection`: Pre-configured WebRTC peer connection

### Daily (`"transport": "daily"`)

Integration with Daily for production video conferencing with rooms, participant management, and Pipecat client compatibility.

```bash theme={null}
python bot.py
# POST /start with {"transport": "daily", "createDailyRoom": true}
# GET /daily redirects the browser into a freshly created room

# Restrict to Daily only
python bot.py -t daily

# Direct connection for testing (bypasses web server)
python bot.py -d

# Enable PSTN dial-in webhook handling
python bot.py --dialin
```

**Runner Arguments**: `DailyRunnerArguments`

* `room_url`: Daily room URL to join
* `token`: Authentication token for the room
* `body`: Request data from /start endpoint (dict) or dial-in webhook data

**Available Endpoints**:

* `GET /daily`: Browser redirect that creates a room and joins it
* `POST /start`: RTVI-compatible endpoint for programmatic access (send `"transport": "daily"`)
* `POST /daily-dialin-webhook`: PSTN dial-in webhook handler (requires `--dialin` flag)

<Note>
  `GET /` redirects to the prebuilt client UI at `/client/` for all transports.
  The Daily browser redirect moved to `GET /daily` in Pipecat 1.3.0.
</Note>

### Telephony (`"transport": "twilio" | "telnyx" | "plivo" | "exotel"`)

Phone call integration through telephony providers. Requires a public webhook endpoint and provider credentials.

```bash theme={null}
# Set up environment variables first
export TWILIO_ACCOUNT_SID=your_account_sid
export TWILIO_AUTH_TOKEN=your_auth_token

# Restrict to Twilio and expose the webhook via a public proxy
python bot.py -t twilio -x yourproxy.ngrok.io
```

The provider-specific XML webhook (`POST /`) is only registered when you select a single telephony transport with `-t` (the XML template needs the `--proxy` hostname). The media WebSocket at `/ws` is always available.

**Environment Variables**:

* Twilio: `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`
* Telnyx: `TELNYX_API_KEY`
* Plivo: `PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN`
* Exotel: None required (no hang-up functionality available)

Environment variables are optional, but when provided will attempt to hang up the call when the session ends.

**Runner Arguments**: `WebSocketRunnerArguments`

* `websocket`: WebSocket connection for audio streaming

The runner automatically detects the telephony provider from incoming WebSocket messages and configures the appropriate serializers and audio settings. Your bot receives a pre-configured FastAPI WebSocket connection ready for telephony audio streaming.

### WebSocket (`"transport": "websocket"`)

Plain (non-telephony) WebSocket connections, for clients such as browser apps that speak protobuf framing directly. Selecting this transport on `/start` returns a `wsUrl` pointing at the `/ws-client` endpoint, which the client then connects to.

```bash theme={null}
python bot.py
# POST /start with {"transport": "websocket"} → returns wsUrl for /ws-client
```

**Runner Arguments**: `WebSocketRunnerArguments`

* `websocket`: WebSocket connection for audio streaming
* `transport_type`: `"websocket"` for plain clients (distinguishes them from telephony connections, which arrive on `/ws`)

Use the `transport_type` field to branch your bot's setup — plain WebSocket clients typically use a `ProtobufFrameSerializer`, whereas telephony clients use a provider-specific serializer.

## Command Line Options

The development runner accepts several command-line arguments to customize its behavior:

```bash theme={null}
python bot.py [OPTIONS]

Options:
  --host TEXT                Server host address (default: localhost)
  --port INTEGER             Server port (default: 7860)
  -t, --transport            Restrict to a single transport and set it as the default
                             for /start: daily, webrtc, twilio, telnyx, plivo, exotel.
                             Omit to support all transports simultaneously (default).
  -x, --proxy TEXT           Public proxy hostname for telephony webhooks (required for telephony)
  --allowed-origins [ORIGIN] Allowed origins for HTTP and WebSocket connections.
                             Can be specified multiple times. Omit or leave empty to allow
                             all origins. Defaults to PIPECAT_ALLOWED_ORIGINS env var.
  --esp32                    Enable SDP munging for ESP32 WebRTC compatibility
  -d, --direct               Connect directly to Daily room for testing (automatically sets transport to daily)
  --dialin                   Enable Daily PSTN dial-in webhook handling
  --whatsapp                 Verify required WhatsApp environment variables are present
  -v, --verbose              Increase logging verbosity
```

### Key Arguments

**`--transport` / `-t`**: Optional. When omitted, the server supports **all** transports at once and the client chooses one per `/start` request. When provided, it restricts the server to a single transport and makes it the default for `/start`; requests for any other transport are rejected with HTTP 400. Valid values:

* `webrtc`: Local WebRTC with browser interface
* `daily`: Daily.co integration with room management
* `twilio`, `telnyx`, `plivo`, `exotel`: Telephony provider integration

**`--proxy` / `-x`**: Required for most telephony transports (Twilio, Telnyx, Plivo). This should be a publicly accessible hostname (like `yourbot.ngrok.io`) that can receive webhooks from telephony providers. The runner automatically strips protocol prefixes (http\://, https\://) if provided. Not required for Exotel, which uses direct WebSocket connections.

**`--direct` / `-d`**: Special mode for Daily that bypasses the web server and connects your bot directly to a Daily room. Useful for quick testing but not recommended for production use.

**`--dialin`**: Enables the `/daily-dialin-webhook` endpoint for handling Daily PSTN dial-in calls. This endpoint receives webhook data from Daily when a phone call dials into your configured phone number, creates a SIP-enabled room, and spawns your bot. (It no longer requires `-t daily`, though dial-in is a Daily feature.)

**`--allowed-origins`**: Restricts which origins can connect to HTTP and WebSocket endpoints. Useful for preventing Cross-Site WebSocket Hijacking (CSWSH) attacks. When set, connections with a missing or disallowed `Origin` header are rejected. Can be specified multiple times for multiple origins. Defaults to the `PIPECAT_ALLOWED_ORIGINS` environment variable (comma-separated). Leave unset to allow all origins.

**`--esp32`**: Enables SDP (Session Description Protocol) modifications needed for ESP32 WebRTC compatibility. Must be used with a specific IP address via `--host`.

### Environment Variables

Different transports require different environment variables:

**Daily**:

* `DAILY_API_KEY`: Daily API key for creating rooms and tokens (required for dial-in)
* `DAILY_SAMPLE_ROOM_URL` (Optional): Existing room URL to use
* `DAILY_API_URL` (Optional): Daily API URL (defaults to [https://api.daily.co/v1](https://api.daily.co/v1))

**Telephony**:

* `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`: Twilio credentials
* `PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN`: Plivo credentials
* `TELNYX_API_KEY`: Telnyx API key

**Security**:

* `PIPECAT_ALLOWED_ORIGINS`: Comma-separated list of allowed origins for HTTP and WebSocket connections. When set, the runner and WebSocket transports reject any connection whose `Origin` header is missing or not in this list. Leave unset or empty to allow all origins. Example: `https://example.com,https://app.example.com`

The runner automatically uses these environment variables when creating transport sessions and authentication tokens.

## Simplifying with the Transport Utility

While the manual approach gives you full control, the `create_transport` utility provides a much cleaner way to handle multiple transports. Instead of writing conditional logic for each transport type, you define transport configurations upfront and let the utility handle the selection:

```python theme={null}
from pipecat.runner.utils import create_transport

# Define transport configurations using factory functions
transport_params = {
    "daily": lambda: DailyParams(
        audio_in_enabled=True,
        audio_out_enabled=True,
    ),
    "webrtc": lambda: TransportParams(
        audio_in_enabled=True,
        audio_out_enabled=True,
    ),
    "twilio": lambda: FastAPIWebsocketParams(
        audio_in_enabled=True,
        audio_out_enabled=True,
        # add_wav_header and serializer handled automatically
    ),
    "websocket": lambda: FastAPIWebsocketParams(
        audio_in_enabled=True,
        audio_out_enabled=True,
        # plain (non-telephony) WebSocket clients, e.g. protobuf framing
    ),
}

async def bot(runner_args):
    """Simplified bot entry point using the transport utility."""
    transport = await create_transport(runner_args, transport_params)
    await run_bot(transport)
```

The utility automatically:

* Detects the telephony provider from WebSocket messages
* Configures the appropriate serializer (Twilio, Telnyx, Plivo, or Exotel)
* Sets up authentication using environment variables
* Handles WebRTC, Daily, and plain WebSocket transport creation

Now your bot supports all seven transport types with just two lines of code in the bot() function.

## RTVI Integration

The development runner provides RTVI (Real-Time Voice Interface) compatible endpoints for the Pipecat client SDKs. This allows you to use the Pipecat client libraries locally during development as well as when deployed to Pipecat Cloud.

### The `/start` Endpoint

The runner exposes a single unified `POST /start` endpoint that works for **all** transports. The client picks the transport via the `"transport"` field in the request body (default: `"webrtc"`); the runner then spawns the bot and returns transport-specific connection details in RTVI-compatible format.

```bash theme={null}
# Start the runner (all transports available)
python bot.py

# The /start endpoint is available at:
# POST http://localhost:7860/start
```

If you started the runner with `-t <transport>`, a `/start` request for any other transport is rejected with HTTP 400.

**Request Format (`"transport": "webrtc"`, the default):**

```json theme={null}
{
  "transport": "webrtc",
  "enableDefaultIceServers": false,
  "body": {
    "custom_data": "your_value",
    "user_id": "user123"
  }
}
```

Returns a `sessionId` (and an `iceConfig` when `enableDefaultIceServers` is set). The bot starts when the WebRTC offer arrives.

<Note>
  `enableDefaultIceServers` is the only ICE option the runner exposes, and it
  adds Google's public STUN server (`stun:stun.l.google.com:19302`) and nothing
  else. The runner builds the `SmallWebRTCConnection` for you, so you cannot
  pass a custom STUN or TURN server from your `bot()` function. To use a custom
  TURN server, run your own signaling server that creates the
  `SmallWebRTCConnection` directly: see [Using ICE servers with the development
  runner](/api-reference/server/services/transport/small-webrtc#using-ice-servers-with-the-development-runner).
</Note>

**Request Format (`"transport": "daily"`):**

```json theme={null}
{
  "transport": "daily",
  "createDailyRoom": true,
  "dailyRoomProperties": {
    "start_video_off": true,
    "start_audio_off": false
  },
  "dailyMeetingTokenProperties": {
    "is_owner": true,
    "user_name": "Bot"
  },
  "body": {
    "custom_data": "your_value",
    "user_id": "user123"
  }
}
```

`createDailyRoom: true` (or a configured `DAILY_ROOM_URL`) makes the runner create a room and token using your `DAILY_API_KEY`. `dailyRoomProperties` and `dailyMeetingTokenProperties` are applied when provided. Response:

```json theme={null}
{
  "dailyRoom": "https://domain.daily.co/room-name",
  "dailyToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "sessionId": "uuid"
}
```

**Request Format (telephony or plain WebSocket):**

For `"transport": "twilio" | "telnyx" | "plivo" | "exotel"` or `"transport": "websocket"`, `/start` returns the WebSocket URL the client should connect to:

```json theme={null}
{
  "wsUrl": "ws://localhost:7860/ws"
}
```

Telephony connections use `/ws`; plain WebSocket clients use `/ws-client` (and also receive a `sessionId`). The bot starts when the connection is established.

### The `/status` Endpoint

`GET /status` reports which transports the running instance accepts — all of them by default, or the single transport passed via `-t`:

```json theme={null}
{
  "status": "ready",
  "transports": [
    "webrtc",
    "daily",
    "twilio",
    "telnyx",
    "plivo",
    "exotel",
    "websocket"
  ]
}
```

### Accessing Request Data in Your Bot

The `body` field from the `/start` request is passed to your bot through `DailyRunnerArguments.body`:

```python theme={null}
async def bot(runner_args: DailyRunnerArguments):
    # Access custom data from the /start request
    custom_data = runner_args.body.get("custom_data")
    user_id = runner_args.body.get("user_id")

    print(f"Bot started for user: {user_id}")
    print(f"Custom data: {custom_data}")

    # Your bot logic here
    transport = DailyTransport(runner_args.room_url, runner_args.token, "Bot")
    await run_bot(transport)
```

### RTVI Client Example

You can use RTVI client libraries to connect to your local development runner:

```javascript theme={null}
import { PipecatClient } from "@pipecat-ai/client-js";
import { DailyTransport } from "@pipecat-ai/daily-transport";

const client = new PipecatClient({
  transport: new DailyTransport(),
  enableMic: true,
  enableCam: false,
});

// Start a session with custom data
await client.connect({ endpoint: "http://localhost:7860/start" });
```

<Info>
  The `/start` endpoint serves every transport. The client SDK's transport (e.g.
  `DailyTransport`, `SmallWebRTCTransport`) determines the `"transport"` value
  sent in the request.
</Info>

## Daily Dial-In Webhook

The development runner can handle Daily PSTN dial-in webhooks when started with the `--dialin` flag. This allows you to test phone call integrations locally before deploying to production.

### Enabling Dial-In Support

```bash theme={null}
python bot.py -t daily --dialin
# Webhook endpoint available at:
# POST http://localhost:7860/daily-dialin-webhook
```

### How It Works

When a phone call dials into your Daily phone number:

1. **Daily sends a webhook** to your configured endpoint with call details
2. **The runner creates a SIP-enabled room** with appropriate configuration
3. **Your bot is spawned** with dial-in context in `runner_args.body`
4. **The call is connected** to the room where your bot is waiting

### Webhook Payload

Daily sends the following data to the `/daily-dialin-webhook` endpoint:

```json theme={null}
{
  "From": "+15551234567",
  "To": "+15559876543",
  "callId": "uuid-call-id",
  "callDomain": "uuid-call-domain",
  "sipHeaders": {}
}
```

### Accessing Dial-In Data in Your Bot

The dial-in webhook data is passed to your bot through `DailyRunnerArguments.body`. You need to parse it and configure the Daily transport with dial-in settings:

```python theme={null}
from pipecat.runner.types import DailyDialinRequest, RunnerArguments
from pipecat.transports.daily.transport import DailyDialinSettings, DailyParams, DailyTransport

async def bot(runner_args: RunnerArguments):
    # Parse the dial-in request from the runner
    request = DailyDialinRequest.model_validate(runner_args.body)

    # Configure Daily transport with dial-in settings
    daily_dialin_settings = DailyDialinSettings(
        call_id=request.dialin_settings.call_id,
        call_domain=request.dialin_settings.call_domain,
    )

    transport = DailyTransport(
        runner_args.room_url,
        runner_args.token,
        "Daily PSTN Dial-in Bot",
        params=DailyParams(
            api_key=request.daily_api_key,
            api_url=request.daily_api_url,
            dialin_settings=daily_dialin_settings,
            audio_in_enabled=True,
            audio_out_enabled=True,
        ),
    )

    # Log caller information if available
    if request.dialin_settings.From:
        print(f"Handling call from: {request.dialin_settings.From}")

    # Your bot logic here
    await run_bot(transport)
```

### Configuring Daily Phone Numbers

To test dial-in locally, you need to:

1. **Configure a Daily phone number** in your Daily dashboard
2. **Set the webhook URL** to your public endpoint (e.g., via ngrok)
3. **Ensure `DAILY_API_KEY` is set** in your environment

```bash theme={null}
# Example with ngrok
ngrok http 7860

# Configure webhook URL in Daily dashboard:
# https://your-subdomain.ngrok.io/daily-dialin-webhook
```

<Note>
  Dial-in is a Daily feature and requires a valid `DAILY_API_KEY` environment
  variable. `--dialin` no longer requires `-t daily`, but you can still pass it
  to restrict the runner to Daily.
</Note>

## Error Handling and Debugging

The development runner includes built-in error handling to help debug issues with incoming requests and webhook payloads.

### 422 Validation Error Logging

When FastAPI receives a malformed request that fails Pydantic validation (HTTP 422 Unprocessable Entity), the runner automatically logs both the validation errors and the raw request body. This makes it much easier to debug malformed payloads from any transport — WhatsApp webhooks, WebRTC signaling, telephony providers, or RTVI clients.

The runner logs:

* The request URL path where the error occurred
* Detailed validation error messages from Pydantic
* The raw request body (up to 5000 characters)

```
ERROR: 422 Validation error on /whatsapp: [{'type': 'missing', 'loc': ['body', 'calls'], ...}]
ERROR: Raw body: {"from": "+1234567890", "to": ...}
```

This automatic logging applies to all runner endpoints and helps identify issues with webhook configurations, client implementations, or API changes without requiring manual debugging code.

## Quick Reference

All transports are available from `python bot.py`; the `-t` form below restricts the server to one. Access columns assume the default `localhost:7860`.

| Transport     | Command                                     | Access                                                       | Environment Variables                               |
| ------------- | ------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------- |
| All (default) | `python bot.py`                             | [http://localhost:7860/client](http://localhost:7860/client) | Per transport (see below)                           |
| WebRTC        | `python bot.py -t webrtc`                   | [http://localhost:7860/client](http://localhost:7860/client) | None                                                |
| Daily         | `python bot.py -t daily`                    | `GET /daily`, `POST /start`                                  | `DAILY_API_KEY`, `DAILY_SAMPLE_ROOM_URL` (Optional) |
| Daily Direct  | `python bot.py -d`                          | Direct connection                                            | `DAILY_API_KEY`, `DAILY_SAMPLE_ROOM_URL` (Optional) |
| Daily Dial-In | `python bot.py --dialin`                    | Phone calls via Daily PSTN                                   | `DAILY_API_KEY` (Required)                          |
| Twilio        | `python bot.py -t twilio -x proxy.ngrok.io` | Phone calls                                                  | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`           |
| Telnyx        | `python bot.py -t telnyx -x proxy.ngrok.io` | Phone calls                                                  | `TELNYX_API_KEY`                                    |
| Plivo         | `python bot.py -t plivo -x proxy.ngrok.io`  | Phone calls                                                  | `PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN`                 |
| Exotel        | `python bot.py -t exotel`                   | Phone calls                                                  | None                                                |
| WebSocket     | `python bot.py`                             | `ws://localhost:7860/ws-client`                              | None                                                |
| ESP32 WebRTC  | `python bot.py --esp32 --host <ESP32_IP>`   | ESP32 WebRTC connection                                      | None                                                |

## Examples

For practical examples of using the development runner with different transports, check out the following:

<Card title="Runner Examples" icon="code" href="https://github.com/pipecat-ai/pipecat-examples/tree/main/runner-examples">
  Explore the examples for different ways to use the development runner with
  various transports.
</Card>
