agentsclimarketplace

Configure telephony

Skill PatterAI/skills/configure-telephony

Agent Skills for the Patter SDK — give your AI agent a phone number. Works in Claude Code, Cursor, OpenClaw, Hermes Agent, Codex, Cline, Goose, Amp, Windsurf, and any harness that consumes the Agent Skills standard. One CLI: npx skills add patterai/skills

Install
npx -y skills add PatterAI/skills --skill configure-telephony

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Configure Twilio or Telnyx as the telephony carrier for a Patter voice agent — buy or use a phone number, set up the carrier console, point the voice/Call-Control webhook at your Patter server (via Cloudflare tunnel, ngrok, or a static URL), and verify webhook signatures. Use when the user is wiring up Twilio, Telnyx, a phone number, a webhook URL, a tunnel, AMD (Answering Machine Detection), call recording, voicemail drop, or DTMF routing — even if they don't say "telephony". Covers both Patter 0.7.0 SDKs (Python and TypeScript).

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

9.8 KB, as published. Nobody here has run it

Configure telephony for Patter

Telephony is the leg between the user's phone and your Patter server. Patter supports two carriers with full feature parity:

CarrierAudioWebhook frameworkWhy pick it
Twiliomulaw 8 kHzTwiMLLargest US/EU coverage, mature ecosystem, AMD + recording.
TelnyxPCM 16 kHzCall Control + Ed25519 sigsLower per-minute cost, native PCM, EU-friendly.

Both expose the same surface in Patter (phone.serve(agent), phone.call(to)). The differences are in how you configure the carrier console.

Decision tree

  1. Already have a Twilio or Telnyx account? Use that one.
  2. Starting fresh in the US? → Twilio. Easier onboarding, larger phone-number inventory.
  3. Starting fresh in Europe / cost-sensitive? → Telnyx.
  4. Need carrier features Patter doesn't bridge yet? Check the per-carrier reference below.

Detailed setup per carrier:

CarrierReference
Twilioreferences/twilio.md
Telnyxreferences/telnyx.md

Read one — not both — once the user has chosen.

Webhook URL options

Whichever carrier you use, the carrier needs a public URL to reach your Patter server. Three patterns, ranked by reliability:

OptionSetupWhen to use
Static URL (webhook_url="https://yourdomain.com")Own subdomain pointed at your server's IP (DNS A record + TLS via Caddy/Cloudflare/etc.)Production. Most reliable.
ngrok (webhook_url="abc.ngrok.io")Paid ngrok account with reserved subdomainAcceptance / dev with stable URL.
Cloudflare quick tunnel (tunnel=True)Patter spawns a tunnel automatically — no setup. URL changes every restart.Dev / local demos only.

webhook_url is set on the Patter constructor, not on serve(). tunnel can be set on either — passing it to serve() is the most common pattern.

Python

from getpatter import Patter, Twilio, Ngrok

# Static (production) — set webhook_url on the constructor
phone = Patter(
    carrier=Twilio(),
    phone_number="+15550001234",
    webhook_url="patter.acme.com",   # no scheme, no trailing slash
)
await phone.serve(agent)

# Cloudflare tunnel (dev) — no webhook_url; pass tunnel=True to serve()
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
await phone.serve(agent, tunnel=True)

# Ngrok with reserved subdomain
phone = Patter(
    carrier=Twilio(),
    phone_number="+15550001234",
    tunnel=Ngrok(hostname="acme-patter.ngrok.io"),
)
await phone.serve(agent)

TypeScript

import { Patter, Twilio, Ngrok } from "getpatter";

// Static (production) — set webhookUrl on the constructor
const phone = new Patter({
  carrier: new Twilio(),
  phoneNumber: "+15550001234",
  webhookUrl: "patter.acme.com",
});
await phone.serve({ agent });

// Cloudflare tunnel (dev) — no webhookUrl; pass tunnel: true to serve()
const dev = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
await dev.serve({ agent, tunnel: true });

// Ngrok with reserved subdomain
const prod = new Patter({
  carrier: new Twilio(),
  phoneNumber: "+15550001234",
  tunnel: new Ngrok({ hostname: "acme-patter.ngrok.io" }),
});
await prod.serve({ agent });

Outbound calls (AMD, voicemail drop)

Place an outbound call through a running server. Since 0.6.3, pass wait=True to block until the call ends and get back a CallResult — its outcome field is exactly what you route on (answered / voicemail / no_answer / busy / failed), derived from real carrier AMD + call-progress signals. machine_detection is on by default since 0.6.3 — pass False only to skip per-call AMD billing. (wait=False, the default, is fire-and-forget and returns None/void; it needs a long-running serve() to keep the call alive.)

Python

from getpatter import Patter, Twilio

# `async with` keeps the local server up for the call's lifetime.
async with Patter(carrier=Twilio(), phone_number="+15550001234") as phone:
    result = await phone.call(
        to="+14155551234",
        agent=agent,
        first_message="Hi, this is Mia from Acme.",
        machine_detection=True,                            # default since 0.6.3
        voicemail_message="Sorry we missed you. Call back at +1...",
        ring_timeout=30,                                    # default 25 s
        wait=True,                                          # → CallResult
    )

    if result.outcome == "voicemail":
        ...                               # AMD hit a machine; voicemail_message was dropped
    elif result.outcome == "answered":
        handle_live_answer(result)            # transcript, cost, duration on result
    # else: no_answer | busy | failed → retry / mark in your campaign

TypeScript

import { Patter, Twilio } from "getpatter";

// `await using` keeps the local server up for the call's lifetime.
await using phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });

const result = await phone.call({
  to: "+14155551234",
  agent,
  firstMessage: "Hi, this is Mia from Acme.",
  machineDetection: true,
  voicemailMessage: "Sorry we missed you. Call back at +1...",
  ringTimeout: 30,
  wait: true,                              // → CallResult
});

if (result.outcome === "voicemail") {
  // AMD hit a machine; voicemailMessage was dropped
} else if (result.outcome === "answered") {
  handleLiveAnswer(result);                // transcript, cost, duration on result
}
// else: no_answer | busy | failed → retry / mark in your campaign

AMD on Twilio: requires the number to have voice capability. Setting a non-empty voicemail_message implicitly enables AMD, so you can omit machine_detection=True if you've set the voicemail. Patter normalizes the Twilio parameter shape automatically since 0.6.3 — older versions required PascalCase like MachineDetection.

Recording is server-wide, not per-call: pass recording=True to phone.serve(...) to enable carrier-side recording for every call routed through that server (inbound and outbound).

Verify webhook signatures

Patter validates carrier webhook signatures by default. You don't have to write code — the validators live in getpatter.handlers.common:

  • Twilio: validate_twilio_signature(headers, body, auth_token, url) — HMAC-SHA1 against the request URL + sorted body params.
  • Telnyx: validate_telnyx_signature(headers, body, public_key) — Ed25519 with anti-replay check (timestamp within ±5 min).

Both return False on missing header (do not raise). Patter's default handler returns 401 on False. Don't disable signature verification.

Gotchas

  • Twilio mulaw 8 kHz vs Telnyx PCM 16 kHz: Patter transcodes both — your agent code never touches raw audio. Don't try to set sample rate manually.
  • Twilio Cloudflare tunnel race: occasionally first call drops because the WSS upgrade hasn't propagated through the CF edge yet. Fixed by bumping grace to 5 s in 0.5.5; still less reliable than a static URL.
  • Telnyx connection ID is required (not just API key). Find it in Portal → Connections → SIP/SDK. Set TELNYX_CONNECTION_ID.
  • Number formats: always E.164 (+15550001234). Patter validates before dialing; non-E.164 raises ValueError.
  • Outbound trunking on Telnyx requires an Outbound Voice Profile attached to the number — Twilio handles this automatically.
  • Recording stores on the carrier side (Twilio Media / Telnyx Storage), never re-uploaded by Patter. URL appears in CallMetrics.recording_url.

Common errors

SymptomFix
Carrier dials but no audioWebhook URL is wrong — carrier can't reach your server. Test with curl <webhook>/health.
Twilio 12300 (TwiML response invalid)Your local server crashed before responding. Check Patter logs for stack trace.
Telnyx "no answer" on outboundOutbound Voice Profile not attached to the number. Set in portal.
signature verification failed warningTunnel URL mismatch — Twilio signed for URL A, Patter validates against URL B. Use static URL or set webhook_url to the same one Twilio has.
AMD always returns "human"Patter uses Twilio async AMD by default for low latency. If you need synchronous AMD accuracy, contact Twilio support to switch your account default.

Related skills

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.