> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpatter.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Local Mode

> Run a fully self-hosted voice AI server with your own telephony and AI provider keys.

# Local Mode (Self-Hosted)

Local mode runs an embedded server on your infrastructure. You bring your own telephony credentials (Twilio, Telnyx, or Plivo) and AI provider keys. No Patter backend required.

## When to Use Local Mode

* You need full control over your infrastructure
* You want to keep all data on your own servers
* You are testing and developing locally
* You need to comply with data residency requirements

## Setup

### 1. Install the SDK

```bash theme={null}
pip install getpatter
```

### 2. Expose your server

Telephony providers need a public URL to reach your local server. The easiest option is the **built-in Cloudflare tunnel** — pass `tunnel=True` (or `tunnel=CloudflareTunnel()`) to `serve()` and Patter creates the tunnel and auto-configures the Twilio webhook for you (requires the `cloudflared` package, see [Tunneling](/dev-tools/tunneling)).

Alternatively, run [ngrok](https://ngrok.com) and pass the hostname as `webhook_url`:

```bash theme={null}
ngrok http 8000
```

### 3. Initialize Patter

```python theme={null}
import os
from dotenv import load_dotenv
from getpatter import Patter, Twilio

load_dotenv()

phone = Patter(
    carrier=Twilio(),                                    # TWILIO_* from env
    phone_number=os.environ["PHONE_NUMBER"],
    webhook_url=os.environ["WEBHOOK_URL"],
)
```

***

## serve()

The `serve()` method starts the embedded server and blocks until it is stopped. It handles inbound calls automatically.

```python theme={null}
await phone.serve(agent, port=8000)
```

### Parameters

| Parameter           | Type               | Default    | Description                                                                                                                                                                                                                             |
| ------------------- | ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent`             | `Agent`            | *required* | The agent configuration to use for all calls. Create with `phone.agent()`.                                                                                                                                                              |
| `port`              | `int`              | `8000`     | TCP port to bind to (1-65535).                                                                                                                                                                                                          |
| `recording`         | `bool`             | `False`    | Enable call recording via the Twilio Recordings API.                                                                                                                                                                                    |
| `local_recording`   | `bool \| str`      | `False`    | Carrier-neutral SDK-side recording: stereo WAV per call (left = caller, right = agent, PCM16 16 kHz). Pass a directory string to set the output directory. See [Local Recording](/python-sdk/features#local-recording-carrier-neutral). |
| `on_call_start`     | `Callable \| None` | `None`     | Async callback fired when a call connects.                                                                                                                                                                                              |
| `on_call_end`       | `Callable \| None` | `None`     | Async callback fired when a call ends.                                                                                                                                                                                                  |
| `on_transcript`     | `Callable \| None` | `None`     | Async callback fired for each utterance.                                                                                                                                                                                                |
| `on_message`        | `Callable \| None` | `None`     | Async callback for pipeline mode. Receives user text, returns agent response.                                                                                                                                                           |
| `voicemail_message` | `str`              | `""`       | Message to speak when AMD detects a machine on outbound calls.                                                                                                                                                                          |
| `on_metrics`        | `Callable \| None` | `None`     | Async callback fired after each conversation turn with real-time cost and latency data. See [Metrics & Cost Tracking](/python-sdk/metrics).                                                                                             |
| `dashboard`         | `bool`             | `True`     | Serve a local metrics dashboard at `http://localhost:{port}/dashboard`.                                                                                                                                                                 |
| `dashboard_token`   | `str`              | `""`       | Bearer token for dashboard authentication. When set, all dashboard routes require this token.                                                                                                                                           |
| `tunnel`            | `bool`             | `False`    | Start a cloudflared tunnel automatically. Requires `cloudflared` on PATH. Mutually exclusive with `webhook_url`.                                                                                                                        |

***

## Making Outbound Calls

In local mode, use `call()` to make outbound calls while the server is running.

**Important:** the server must be fully initialized before you call `phone.call()`. Use `await phone.ready` — it resolves once the tunnel is up, the embedded server is in `listen` state, and the carrier webhook is configured. This is the reliable replacement for `asyncio.sleep()` guesswork:

```python theme={null}
import asyncio

async def main():
    # Start the server in the background — don't await it yet.
    server_task = asyncio.create_task(phone.serve(agent, port=8000))

    # Block until tunnel + HTTP server + carrier webhook are all wired up.
    # Resolves to the public webhook hostname as a string.
    await phone.ready

    try:
        # Now safe to make outbound calls
        await phone.call(
            to="+15550009876",
            agent=agent,
            machine_detection=True,
            voicemail_message="Please call us back at 555-000-1234.",
        )
    finally:
        # Clean shutdown
        await phone.disconnect()
        server_task.cancel()

asyncio.run(main())
```

`phone.ready` rejects with the underlying exception if `serve()` fails before the server reaches `listen` state, so you get an immediate error instead of a hanging `await`.

**Advanced — tunnel hostname only:** if you only need the public URL (for example, to register a webhook manually) without waiting for the HTTP server to be in `listen` state, use `await phone.tunnel_ready` instead. It resolves earlier but a `phone.call()` placed immediately afterwards can race the WebSocket upgrade path and produce a dropped call on answer.

### call() Parameters (Local Mode)

All keyword arguments are **snake\_case** (e.g. `machine_detection=`, `ring_timeout=`, `on_machine_detection=`).

| Parameter              | Type                                                                  | Default    | Description                                                                                                                                                                                                                                                       |
| ---------------------- | --------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `to`                   | `str`                                                                 | *required* | Phone number to call (E.164 format).                                                                                                                                                                                                                              |
| `agent`                | `Agent`                                                               | *required* | Agent instance to use for this call.                                                                                                                                                                                                                              |
| `first_message`        | `str`                                                                 | `""`       | What the AI says when the callee answers.                                                                                                                                                                                                                         |
| `from_number`          | `str`                                                                 | `""`       | Override the configured phone number.                                                                                                                                                                                                                             |
| `machine_detection`    | `bool`                                                                | `True`     | Enable answering machine detection. Defaults **on** since 0.6.2 — on Twilio Patter uses `MachineDetection=DetectMessageEnd` + Async AMD so there is no answer-latency penalty on human pickups. Pass `False` to skip per-call AMD billing for known destinations. |
| `on_machine_detection` | `Callable[[MachineDetectionResult], Awaitable[None] \| None] \| None` | `None`     | Fires once when the carrier reports the AMD outcome (`human` or `machine`).                                                                                                                                                                                       |
| `voicemail_message`    | `str`                                                                 | `""`       | Message to leave on voicemail. A non-empty value also implicitly enables `machine_detection`.                                                                                                                                                                     |
| `ring_timeout`         | `int \| None`                                                         | `25`       | Ring timeout in seconds before treating the call as no-answer. Defaults to 25 s — production-recommended. Pass `60` for legacy carrier-default parity, or `None` to omit the parameter entirely (carrier picks its own default).                                  |

***

## EmbeddedServer

The `EmbeddedServer` is the internal class that powers `serve()`. You typically do not interact with it directly, but it is useful to understand for advanced use cases.

| Property            | Type          | Description                              |
| ------------------- | ------------- | ---------------------------------------- |
| `config`            | `LocalConfig` | Telephony and AI provider configuration. |
| `agent`             | `Agent`       | The agent configuration.                 |
| `recording`         | `bool`        | Whether call recording is enabled.       |
| `voicemail_message` | `str`         | Voicemail message for AMD.               |

### Methods

| Method        | Description                  |
| ------------- | ---------------------------- |
| `start(port)` | Start the server (blocking). |
| `stop()`      | Gracefully stop the server.  |

***

## LocalConfig

The `LocalConfig` dataclass holds all provider credentials for local mode. It is created automatically from the `Patter` constructor — you don't normally need to touch it directly.

| Field                                                                                      | Type          | Description                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `telephony_provider`                                                                       | `str`         | `"twilio"` or `"telnyx"` (auto-detected from the `carrier=` instance).                                                                                                          |
| `twilio_sid`                                                                               | `str`         | Twilio Account SID (unpacked from `Twilio(...)`).                                                                                                                               |
| `twilio_token`                                                                             | `str`         | Twilio Auth Token (unpacked from `Twilio(...)`).                                                                                                                                |
| `telnyx_key`                                                                               | `str`         | Telnyx API key (unpacked from `Telnyx(...)`).                                                                                                                                   |
| `telnyx_connection_id`                                                                     | `str`         | Telnyx Call Control Application ID (unpacked from `Telnyx(...)`).                                                                                                               |
| `telnyx_public_key`                                                                        | `str`         | Telnyx Ed25519 public key for webhook signature verification (optional).                                                                                                        |
| `openai_key`                                                                               | `str`         | OpenAI API key (resolved from `OpenAIRealtime(...)` or `OPENAI_API_KEY`).                                                                                                       |
| `elevenlabs_key`                                                                           | `str`         | ElevenLabs API key.                                                                                                                                                             |
| `deepgram_key`                                                                             | `str`         | Deepgram API key.                                                                                                                                                               |
| `cartesia_key`, `rime_key`, `lmnt_key`, `soniox_key`, `speechmatics_key`, `assemblyai_key` | `str`         | Provider-specific keys backfilled from the matching constructor or env var.                                                                                                     |
| `phone_number`                                                                             | `str`         | Phone number in E.164 format.                                                                                                                                                   |
| `webhook_url`                                                                              | `str`         | Public hostname (no scheme).                                                                                                                                                    |
| `require_signature`                                                                        | `bool`        | When `True` (default), inbound webhooks with missing credentials return HTTP 503 instead of silently accepting. Disable only for local mock-provider testing.                   |
| `persist_root`                                                                             | `str \| None` | Resolved persistence path for the dashboard's on-disk call history, or `None` to disable. Set by the `persist=` argument on `Patter(...)` (with `PATTER_LOG_DIR` env fallback). |

***

## Disconnecting

Call `disconnect()` to stop the embedded server gracefully:

```python theme={null}
await phone.disconnect()
```

***

## Complete Example

```python theme={null}
import os
import asyncio
from dotenv import load_dotenv
from getpatter import Patter, Twilio, OpenAIRealtime, Tool, Guardrail

load_dotenv()

phone = Patter(
    carrier=Twilio(),                                   # TWILIO_* from env
    phone_number=os.environ["PHONE_NUMBER"],
    webhook_url=os.environ["WEBHOOK_URL"],
)

async def schedule_appointment(args: dict, ctx: dict) -> dict:
    # Your reservation system here.
    return {"confirmation": "ACME-123", "date": args["date"], "time": args["time"]}

agent = phone.agent(
    engine=OpenAIRealtime(voice="nova"),                # OPENAI_API_KEY from env
    system_prompt="""You are a friendly receptionist for Acme Corp.
Help callers schedule appointments, answer general questions, and transfer to the right department.
Transfer billing questions to +15550001111.
Transfer technical support to +15550002222.""",
    first_message="Thank you for calling Acme Corp! How can I help you today?",
    tools=[
        Tool(
            name="schedule_appointment",
            description="Schedule an appointment for the caller.",
            parameters={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Caller's full name"},
                    "date": {"type": "string", "description": "Preferred date (YYYY-MM-DD)"},
                    "time": {"type": "string", "description": "Preferred time (HH:MM)"},
                    "reason": {"type": "string", "description": "Reason for appointment"},
                },
                "required": ["name", "date", "time"],
            },
            handler=schedule_appointment,
        ),
    ],
    guardrails=[
        Guardrail(
            name="No legal advice",
            blocked_terms=["lawsuit", "sue", "legal action"],
            replacement="I'm not able to provide legal guidance. Let me transfer you to our legal department.",
        ),
    ],
)

async def on_call_start(event):
    print(f"[CALL START] {event['direction']} call {event['call_id']}")
    print(f"  From: {event['caller']} -> To: {event['callee']}")

async def on_call_end(event):
    print(f"[CALL END] {event['call_id']}")
    for entry in event["transcript"]:
        print(f"  [{entry['role']}]: {entry['text']}")

async def on_transcript(event):
    print(f"  [{event['role']}]: {event['text']}")

async def main():
    await phone.serve(
        agent,
        port=8000,
        recording=True,
        on_call_start=on_call_start,
        on_call_end=on_call_end,
        on_transcript=on_transcript,
        voicemail_message="Hi, this is Acme Corp. Please call us back at your earliest convenience.",
    )

asyncio.run(main())
```
