Skip to main content

Connect AI agents to phone numbers with 10 lines of code

Project description

Patter

Connect AI agents to phone numbers with 10 lines of code

QuickstartFeaturesInstallationDocumentationSelf-Hosting

Python 3.11+ TypeScript 5+ MIT License


Patter is an open-source platform that gives your AI agent a voice and a phone number. Point it at any function that returns a string, and Patter handles the rest: telephony, speech-to-text, text-to-speech, and real-time audio streaming.

Quickstart

Python
import asyncio
from patter import Patter, IncomingMessage

async def on_message(msg: IncomingMessage) -> str:
    # Your agent logic here — return what the AI should say
    return f"You said: {msg.text}"

async def main():
    phone = Patter(api_key="pt_xxx")
    await phone.connect(on_message=on_message)  # starts listening for inbound calls

asyncio.run(main())
TypeScript
import { Patter } from "getpatter";

const phone = new Patter({ apiKey: "pt_xxx" });

await phone.connect({
  onMessage: async (msg) => {
    // Your agent logic here — return what the AI should say
    return `You said: ${msg.text}`;
  },
});

Local Mode (No Cloud Required)

Run Patter entirely in your process — no Patter account, no cloud backend.

Python
import asyncio
from patter import Patter

async def main():
    phone = Patter(
        mode="local",
        twilio_sid="AC...", twilio_token="...",
        openai_key="sk-...",
        phone_number="+1...",
        webhook_url="xxx.ngrok-free.dev",
    )

    agent = phone.agent(
        system_prompt="You are a friendly customer service agent for Acme Corp.",
        voice="alloy",
        first_message="Hello! Thanks for calling. How can I help?",
    )

    print("Listening for calls...")
    await phone.serve(agent=agent, port=8000)

asyncio.run(main())
TypeScript
import { Patter } from "getpatter";

const phone = new Patter({
  mode: "local",
  twilioSid: "AC...", twilioToken: "...",
  openaiKey: "sk-...",
  phoneNumber: "+1...",
  webhookUrl: "xxx.ngrok-free.dev",
});

const agent = phone.agent({
  systemPrompt: "You are a friendly customer service agent for Acme Corp.",
  voice: "alloy",
  firstMessage: "Hello! Thanks for calling. How can I help?",
});

console.log("Listening for calls...");
await phone.serve({ agent, port: 8000 });

Local vs Cloud

Cloud Mode Local Mode
Setup Patter API key only Twilio/Telnyx + OpenAI keys
Infrastructure Managed by Patter Runs in your process
Backend wss://api.getpatter.com Built-in (FastAPI / Express)
Webhook Configured automatically Requires public URL (e.g. ngrok)
Voice modes All three All three
Best for Production, multi-tenant Development, on-prem, full control

Features

Voice

  • Three voice modes: OpenAI Realtime, ElevenLabs ConvAI, Pipeline (any STT + TTS)
  • Any STT: Deepgram, OpenAI Whisper
  • Any TTS: ElevenLabs, OpenAI TTS
  • Natural barge-in with mark-based audio tracking
  • DTMF keypad input forwarded to agent as [DTMF: 1]

Agent

  • Bring your own agent (any LLM in pipeline mode)
  • System prompt with dynamic {variable} substitution
  • Conversation history tracked per call (data.history in all callbacks)
  • Tool calling via webhooks with automatic 3x retry
  • Built-in tools: transfer_call, end_call (auto-injected)

Telephony

  • Twilio and Telnyx carriers
  • Inbound and outbound calls
  • Call transfer to humans (transfer_call system tool)
  • Call recording (recording: true in serve())
  • Answering machine detection (machineDetection: true for outbound)
  • Voicemail drop (voicemailMessage: "..." plays on voicemail detection)
  • Custom parameters passthrough via TwiML

Developer Experience

  • pip install patter / npm install getpatter
  • 10 lines of code to connect an agent to a phone
  • Local mode (embedded, no backend) + Cloud mode
  • Python + TypeScript SDKs with full parity
  • MCP server for Claude Desktop
  • Open-source (MIT)

Complete Example

const phone = new Patter({
  mode: 'local',
  twilioSid: process.env.TWILIO_SID,
  twilioToken: process.env.TWILIO_TOKEN,
  openaiKey: process.env.OPENAI_KEY,
  phoneNumber: '+16592214527',
  webhookUrl: 'your-domain.ngrok-free.dev',
});

const agent = phone.agent({
  systemPrompt: `You are a customer service agent for Acme Corp.
The customer is {customer_name} with order #{order_id}.
Check inventory before answering stock questions.
Transfer to a human if the customer is upset.`,
  voice: 'alloy',
  language: 'en',
  firstMessage: 'Hi {customer_name}! How can I help with order #{order_id}?',
  variables: {
    customer_name: 'John',
    order_id: '12345',
  },
  tools: [{
    name: 'check_inventory',
    description: 'Check product stock',
    parameters: { type: 'object', properties: { product: { type: 'string' } } },
    webhookUrl: 'https://api.acme.com/inventory',
  }],
  // Built-in: transfer_call, end_call (auto-injected)
});

await phone.serve({
  agent,
  port: 8000,
  recording: true,
  onCallStart: async (data) => console.log(`Call from ${data.caller}`),
  onCallEnd: async (data) => console.log(`Transcript: ${data.transcript?.length} turns`),
  onTranscript: async (data) => console.log(`${data.role}: ${data.text}`),
});

// Outbound with machine detection
await phone.call({
  to: '+1234567890',
  machineDetection: true,
  voicemailMessage: 'Hi, please call us back at 555-0123.',
});

How It Works

Your Code (on_message handler)
        │
        ▼
   Patter SDK  ──WebSocket──►  Patter Backend  ──────────────────────────────┐
                                      │                                       │
                              ┌───────┴────────┐                              │
                              ▼                ▼                              ▼
                          STT Engine       TTS Engine          Telephony Provider
                       (Deepgram /      (ElevenLabs /          (Twilio / Telnyx)
                        Whisper /        OpenAI TTS)                  │
                       OpenAI RT)              │                       │
                              │               └───────────────────────►│
                              └────────────────────────────────────────►│
                                                                        ▼
                                                                  Phone Call

The audio path: Phone → Telephony → WebSocket → Backend → STT → your handler → TTS → Backend → WebSocket → Phone

Installation

# Python
pip install patter

# TypeScript / Node.js
npm install getpatter

Documentation

Inbound Calls (AI answers the phone)

Python
import asyncio
from patter import Patter, IncomingMessage

async def agent(msg: IncomingMessage) -> str:
    if "hours" in msg.text.lower():
        return "We're open Monday through Friday, 9 to 5."
    return "How can I help you today?"

async def main():
    phone = Patter(api_key="pt_xxx")
    await phone.connect(
        on_message=agent,
        on_call_start=lambda data: print(f"Call from {data['caller']}"),
        on_call_end=lambda data: print("Call ended"),
    )
    await asyncio.Event().wait()  # keep the process alive

asyncio.run(main())
TypeScript
import { Patter } from "getpatter";

const phone = new Patter({ apiKey: "pt_xxx" });

await phone.connect({
  onMessage: async (msg) => {
    if (msg.text.toLowerCase().includes("hours")) {
      return "We're open Monday through Friday, 9 to 5.";
    }
    return "How can I help you today?";
  },
  onCallStart: (data) => console.log(`Call from ${data.caller}`),
  onCallEnd: () => console.log("Call ended"),
});

Outbound Calls (AI calls someone)

Python
import asyncio
from patter import Patter, IncomingMessage

async def agent(msg: IncomingMessage) -> str:
    return "Thanks for picking up. This is a reminder about your appointment tomorrow."

async def main():
    phone = Patter(api_key="pt_xxx")
    await phone.connect(on_message=agent)
    await phone.call(
        to="+14155551234",
        first_message="Hi, this is an automated reminder from Acme Corp.",
    )

asyncio.run(main())
TypeScript
import { Patter } from "getpatter";

const phone = new Patter({ apiKey: "pt_xxx" });

await phone.connect({
  onMessage: async () =>
    "Thanks for picking up. This is a reminder about your appointment tomorrow.",
});

await phone.call({
  to: "+14155551234",
  firstMessage: "Hi, this is an automated reminder from Acme Corp.",
});

Outbound with Machine Detection + Voicemail Drop

Python
await phone.call(
    to="+14155551234",
    first_message="Hi, this is an automated reminder from Acme Corp.",
    machine_detection=True,
    voicemail_message="Hi, we tried to reach you. Please call us back at 555-0123.",
)
TypeScript
await phone.call({
  to: "+14155551234",
  firstMessage: "Hi, this is an automated reminder from Acme Corp.",
  machineDetection: true,
  voicemailMessage: "Hi, we tried to reach you. Please call us back at 555-0123.",
});

Dynamic Variables in Prompts

Inject call-specific data into system prompts and first messages using {variable} placeholders.

Python
agent = phone.agent(
    system_prompt="You are helping {customer_name}, account #{account_id}.",
    first_message="Hi {customer_name}! How can I help you today?",
    variables={
        "customer_name": "Jane",
        "account_id": "A-789",
    },
)
TypeScript
const agent = phone.agent({
  systemPrompt: "You are helping {customer_name}, account #{account_id}.",
  firstMessage: "Hi {customer_name}! How can I help you today?",
  variables: {
    customer_name: "Jane",
    account_id: "A-789",
  },
});

Tool Calling via Webhooks

Agents can call external APIs mid-conversation. Patter POSTs to your webhook URL and retries up to 3 times on failure.

Python
agent = phone.agent(
    system_prompt="You are a booking assistant. Check availability before confirming.",
    tools=[{
        "name": "check_availability",
        "description": "Check appointment availability for a given date",
        "parameters": {
            "type": "object",
            "properties": {
                "date": {"type": "string", "description": "ISO date, e.g. 2025-06-15"},
            },
            "required": ["date"],
        },
        "webhook_url": "https://api.example.com/availability",
    }],
)
TypeScript
const agent = phone.agent({
  systemPrompt: "You are a booking assistant. Check availability before confirming.",
  tools: [{
    name: "check_availability",
    description: "Check appointment availability for a given date",
    parameters: {
      type: "object",
      properties: {
        date: { type: "string", description: "ISO date, e.g. 2025-06-15" },
      },
      required: ["date"],
    },
    webhookUrl: "https://api.example.com/availability",
  }],
});

Built-in Tools: Transfer & End Call

transfer_call and end_call are automatically injected into every agent — no configuration needed.

  • The agent calls transfer_call when it decides to route to a human (e.g. "Let me transfer you now.")
  • The agent calls end_call when the conversation is complete (e.g. after a confirmed booking.)

Call Recording

Python
await phone.serve(agent=agent, port=8000, recording=True)
TypeScript
await phone.serve({ agent, port: 8000, recording: true });

Conversation History

Every callback receives data.history — the full conversation so far as a list of {role, text} turns.

Python
await phone.serve(
    agent=agent,
    port=8000,
    on_transcript=lambda data: print(f"[{data['role']}] {data['text']}"),
    on_call_end=lambda data: print(f"Full history: {data['history']}"),
)
TypeScript
await phone.serve({
  agent,
  port: 8000,
  onTranscript: (data) => console.log(`[${data.role}] ${data.text}`),
  onCallEnd: (data) => console.log(`Full history:`, data.history),
});

Custom Voice (choose your providers)

Python
phone = Patter(api_key="pt_xxx", backend_url="ws://localhost:8000")

await phone.connect(
    on_message=agent,
    provider="twilio",
    provider_key="ACxxxxxxxx",
    provider_secret="your_auth_token",
    number="+14155550000",
    stt=Patter.deepgram(api_key="dg_xxx", language="en"),
    tts=Patter.elevenlabs(api_key="el_xxx", voice="rachel"),
)
TypeScript
const phone = new Patter({ apiKey: "pt_xxx", backendUrl: "ws://localhost:8000" });

await phone.connect({
  onMessage: agent,
  provider: "twilio",
  providerKey: "ACxxxxxxxx",
  providerSecret: "your_auth_token",
  number: "+14155550000",
  stt: Patter.deepgram({ apiKey: "dg_xxx", language: "en" }),
  tts: Patter.elevenlabs({ apiKey: "el_xxx", voice: "rachel" }),
});

Voice Modes

Mode Latency Quality Best For
OpenAI Realtime Lowest High Fluid, low-latency conversations
Deepgram + ElevenLabs Low High Independent control over STT and TTS
ElevenLabs ConvAI Low High ElevenLabs-managed conversation flow

The voice mode is configured on the backend. Your on_message handler works identically regardless of mode.

MCP Server (Claude Desktop)

Patter ships an MCP server so you can control calls directly from Claude Desktop.

{
  "mcpServers": {
    "patter": {
      "command": "patter-mcp",
      "env": { "PATTER_API_KEY": "pt_xxx" }
    }
  }
}

Self-Hosting

Run the full stack yourself — no Patter Cloud account needed.

# 1. Clone the repo
git clone https://github.com/your-org/patter
cd patter

# 2. Copy env and fill in your keys
cp .env.example .env

# 3. Start the backend
cd backend
pip install -e ".[dev]"
alembic upgrade head
uvicorn app.main:app --reload

Then point your SDK at your local backend:

phone = Patter(
    api_key="pt_xxx",
    backend_url="ws://localhost:8000",
    rest_url="http://localhost:8000",
)

Required environment variables:

Variable Description
PATTER_DATABASE_URL PostgreSQL connection string
PATTER_ENCRYPTION_KEY Key for encrypting stored provider credentials
PATTER_SECRET_KEY JWT / HMAC signing secret

See backend/.env.example for the full list.

API Reference

Patter (Python & TypeScript)

Method Description
Patter(api_key, backend_url?, rest_url?) Create client. backend_url defaults to wss://api.getpatter.com.
connect(on_message, ...) Connect and start receiving calls. Blocks until disconnected.
call(to, first_message?, machine_detection?, voicemail_message?, ...) Place an outbound call.
disconnect() Gracefully close the connection.

serve() options:

Option Type Description
agent Agent Agent configuration to use for calls
port int Port to listen on
recording bool Enable call recording via the telephony provider
onCallStart callable Called when a call connects; receives data.caller, data.call_id
onCallEnd callable Called when a call ends; receives data.history, data.transcript, data.duration
onTranscript callable Called on each transcript turn; receives data.role, data.text, data.history

agent() options:

Option Type Description
system_prompt str Prompt with optional {variable} placeholders
variables dict Values substituted into system_prompt and first_message
voice str TTS voice name
language str BCP-47 language code
first_message str Opening message (supports {variable} placeholders)
tools list Tool definitions with name, description, parameters, webhook_url

call() options:

Option Type Description
to str Destination phone number
first_message str Opening message for the outbound call
machine_detection bool Enable answering machine detection
voicemail_message str Message to play when voicemail is detected

Static provider helpers (for self-hosted mode):

Helper Type Description
Patter.deepgram(api_key, language?) STT Deepgram Nova
Patter.whisper(api_key, language?) STT OpenAI Whisper
Patter.elevenlabs(api_key, voice?) TTS ElevenLabs
Patter.openai_tts(api_key, voice?) TTS OpenAI TTS

IncomingMessage

Field Type Description
text str Transcribed speech from the caller (includes [DTMF: N] for keypad presses)
call_id str Unique identifier for the current call
history list Conversation turns so far: [{role, text}, ...]

Contributing

Pull requests are welcome.

# Run tests
cd backend && pytest tests/ -v
cd sdk && pytest tests/ -v

# Install dev dependencies
cd backend && pip install -e ".[dev]"
cd sdk && pip install -e ".[dev]"

Please open an issue before submitting large changes so we can discuss the approach first.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

getpatter-0.4.1.tar.gz (108.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

getpatter-0.4.1-py3-none-any.whl (86.3 kB view details)

Uploaded Python 3

File details

Details for the file getpatter-0.4.1.tar.gz.

File metadata

  • Download URL: getpatter-0.4.1.tar.gz
  • Upload date:
  • Size: 108.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for getpatter-0.4.1.tar.gz
Algorithm Hash digest
SHA256 88e7697c3e2c6149afbb8bd0ede2527439242718b97f3e9270d5b7b2bd035118
MD5 2828bdfc6111d0613d6699d47f159dfb
BLAKE2b-256 f524eefd250f4d79bb5c63b8015b2e3680fc596bdac42a9b35a6cad1cce977b5

See more details on using hashes here.

Provenance

The following attestation bundles were made for getpatter-0.4.1.tar.gz:

Publisher: release.yml on PatterAI/Patter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file getpatter-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: getpatter-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 86.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for getpatter-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 00917431d694f5f9c388a42bc281e61c484e2a4dcb60b4553fd02166899b78ee
MD5 c652b2a5f17d4e248af2706bdbb34b58
BLAKE2b-256 689e9fa07a4bf3be81bbf533bae7e9e6ecbf5b1b8443bcadbb7b7aad5d2c77ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for getpatter-0.4.1-py3-none-any.whl:

Publisher: release.yml on PatterAI/Patter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page