Skip to main content

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 (Self-Hosted)

Local mode runs an embedded server on your infrastructure. You bring your own telephony credentials (Twilio or Telnyx) 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

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). Alternatively, run ngrok and pass the hostname as webhook_url:
ngrok http 8000

3. Initialize Patter

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.
await phone.serve(agent, port=8000)

Parameters

ParameterTypeDefaultDescription
agentAgentrequiredThe agent configuration to use for all calls. Create with phone.agent().
portint8000TCP port to bind to (1-65535).
recordingboolFalseEnable call recording via the Twilio Recordings API.
on_call_startCallable | NoneNoneAsync callback fired when a call connects.
on_call_endCallable | NoneNoneAsync callback fired when a call ends.
on_transcriptCallable | NoneNoneAsync callback fired for each utterance.
on_messageCallable | NoneNoneAsync callback for pipeline mode. Receives user text, returns agent response.
voicemail_messagestr""Message to speak when AMD detects a machine on outbound calls.
on_metricsCallable | NoneNoneAsync callback fired after each conversation turn with real-time cost and latency data. See Metrics & Cost Tracking.
dashboardboolTrueServe a local metrics dashboard at http://localhost:{port}/dashboard.
dashboard_tokenstr""Bearer token for dashboard authentication. When set, all dashboard routes require this token.
tunnelboolFalseStart 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:
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=).
ParameterTypeDefaultDescription
tostrrequiredPhone number to call (E.164 format).
agentAgentrequiredAgent instance to use for this call.
first_messagestr""What the AI says when the callee answers.
from_numberstr""Override the configured phone number.
machine_detectionboolTrueEnable 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_detectionCallable[[MachineDetectionResult], Awaitable[None] | None] | NoneNoneFires once when the carrier reports the AMD outcome (human or machine).
voicemail_messagestr""Message to leave on voicemail. A non-empty value also implicitly enables machine_detection.
ring_timeoutint | None25Ring 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.
PropertyTypeDescription
configLocalConfigTelephony and AI provider configuration.
agentAgentThe agent configuration.
recordingboolWhether call recording is enabled.
voicemail_messagestrVoicemail message for AMD.

Methods

MethodDescription
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.
FieldTypeDescription
telephony_providerstr"twilio" or "telnyx" (auto-detected from the carrier= instance).
twilio_sidstrTwilio Account SID (unpacked from Twilio(...)).
twilio_tokenstrTwilio Auth Token (unpacked from Twilio(...)).
telnyx_keystrTelnyx API key (unpacked from Telnyx(...)).
telnyx_connection_idstrTelnyx Call Control Application ID (unpacked from Telnyx(...)).
telnyx_public_keystrTelnyx Ed25519 public key for webhook signature verification (optional).
openai_keystrOpenAI API key (resolved from OpenAIRealtime(...) or OPENAI_API_KEY).
elevenlabs_keystrElevenLabs API key.
deepgram_keystrDeepgram API key.
cartesia_key, rime_key, lmnt_key, soniox_key, speechmatics_key, assemblyai_keystrProvider-specific keys backfilled from the matching constructor or env var.
phone_numberstrPhone number in E.164 format.
webhook_urlstrPublic hostname (no scheme).
require_signatureboolWhen True (default), inbound webhooks with missing credentials return HTTP 503 instead of silently accepting. Disable only for local mock-provider testing.
persist_rootstr | NoneResolved 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:
await phone.disconnect()

Complete Example

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