configure-telephony — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited configure-telephony (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Telephony is the leg between the user's phone and your Patter server. Patter supports two carriers with full feature parity:
| Carrier | Audio | Webhook framework | Why pick it |
|---|---|---|---|
| Twilio | mulaw 8 kHz | TwiML | Largest US/EU coverage, mature ecosystem, AMD + recording. |
| Telnyx | PCM 16 kHz | Call Control + Ed25519 sigs | Lower 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.
reference below.
Detailed setup per carrier:
| Carrier | Reference |
|---|---|
| Twilio | references/twilio.md |
| Telnyx | references/telnyx.md |
Read one — not both — once the user has chosen.
Whichever carrier you use, the carrier needs a public URL to reach your Patter server. Three patterns, ranked by reliability:
| Option | Setup | When 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 subdomain | Acceptance / 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.
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)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 });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 in 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.)
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 in 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 campaignimport { 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 campaignAMD 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 in 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).
Patter validates carrier webhook signatures by default. You don't have to write code — the validators live in getpatter.handlers.common:
validate_twilio_signature(headers, body, auth_token, url) — HMAC-SHA1against the request URL + sorted body params.
validate_telnyx_signature(headers, body, public_key) — Ed25519with 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.
agent code never touches raw audio. Don't try to set sample rate manually.
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.
Portal → Connections → SIP/SDK. Set TELNYX_CONNECTION_ID.
+15550001234). Patter validates beforedialing; non-E.164 raises ValueError.
to the number — Twilio handles this automatically.
never re-uploaded by Patter. URL appears in CallMetrics.recording_url.
| Symptom | Fix |
|---|---|
| Carrier dials but no audio | Webhook 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 outbound | Outbound Voice Profile not attached to the number. Set in portal. |
signature verification failed warning | Tunnel 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. |
setup-patter — install + provider API keys (do this first).build-voice-agent — define what the agent says once a call connects.inspect-calls-and-metrics — dashboard, recordings, cost.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.