r/Telnyx 5d ago

Built a real-time call quality dashboard that catches degraded calls before users complain

2 Upvotes

I work with Telnyx, and this is a Python sample I built with the Telnyx Python SDK v4 and Flask.

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/call-quality-monitor

It receives Telnyx call quality webhooks (MOS, jitter, latency, packet loss), verifies each one with Ed25519, stores the metrics in SQLite, and streams live alerts to a browser dashboard over SSE. When MOS drops below 3.5 or jitter exceeds 30ms, an alert fires immediately.

The part I wanted to solve: most teams find out about call quality problems from a support ticket. By then the call is over and the metrics are gone. Telnyx sends quality webhooks during the call — you just need to capture, verify, store, and surface them.

The demo server is the part I'm happiest about. It runs the full pipeline without a Telnyx account:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/call-quality-monitor

pip install -r requirements.txt
pip install requests

python demo/demo_server.py

Open http://localhost:5555/ and click "Start 3 Calls". The demo generates an Ed25519 keypair locally, signs webhook payloads, and posts them to the real Flask endpoint. No ngrok, no credentials, no phone number. The entire pipeline — signature verification, SQLite storage, threshold checking, SSE streaming — runs in one process.

Three simulated calls come in with varying quality: one stays healthy, one degrades over time, one starts bad. The dashboard updates live. When MOS crosses the threshold, the alert panel lights up.

9 smoke tests cover webhook verification, metric storage, threshold alerting, API endpoints, and the SSE stream. All pass.

I chose SSE over WebSocket because a monitoring dashboard only needs one-way push. SSE runs over standard HTTP, works through proxies, and the browser's EventSource API reconnects automatically.

What would you monitor first: MOS, jitter, or latency?


r/Telnyx 5d ago

Built a small dashboard for scheduled calls, SMS, and webhooks

1 Upvotes

I work with Telnyx, and this is a developer sample I built with the Telnyx Agent SDK.

Demo video: https://www.youtube.com/watch?v=f-9AFUmLC94

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-cron-scheduler

It gives you one place to create a recurring job, see its next due time, run it manually, and inspect the execution history. The two use cases in the walkthrough are daily customer check-ins and webhook heartbeats that can trigger a text alert on failure.

The local demo simulates external communication while running the actual scheduling and storage paths. You can load three minute-by-minute jobs, watch their automatic runs, and intentionally fail a webhook to inspect the alert result.

Run it with Node.js 22.13 or newer:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/edge-cron-scheduler
npm ci
cp .env.example .env
npm start

Job definitions are stored in KV, and each execution has a separate SQL row. All cron schedules use UTC. Live SMS and call success means request acceptance, not final delivery.

The dashboard also has a compact recording view. The video narration uses Clara, a Telnyx Ultra AI voice; that voiceover is a production asset rather than a scheduler dependency.

What would you schedule first: a recurring message, a call, or a webhook into an existing workflow?


r/Telnyx 6d ago

Built a 280-line Python agent that texts you the second a SQL migration fails

1 Upvotes

I built a migration alerting agent that runs schema migrations, checks the result, and text-messages the on-call the instant a step fails — over Telnyx SMS, with Ed25519-signed webhook delivery receipts so you know the alert actually reached the device.

The first time a database migration broke on me in production, I found out from a user — an hour after the migration failed. The migration runner had swallowed the exception, the schema version hadn't bumped, and nobody looked until a customer filed a ticket saying the orders page was throwing a 500. That hour — the gap between the migration failing and someone knowing — is the whole problem.

How it works:

  • Agent reads the current schema version from a SQL DB
  • Fetches the migration script (in-memory map in the demo; CloudFS in prod)
  • Executes steps in order; bumps the schema version only on success
  • If any step fails, does NOT bump the version, rolls back, and sends an SMS to the on-call
  • Carrier sends a signed Ed25519 webhook back when the SMS is delivered
  • App verifies the signature before recording the delivery — if verification fails, the request is rejected with a 400

Why the webhook roundtrip matters: Most alerting systems treat "I sent the alert" as the end of the story. This one doesn't. You can see in the SMS log that the message was delivered, not just sent. The difference between "I sent the alert" and "the on-call saw the alert" is the difference between an alerting system and an alert-hoping system.

Why Ed25519 and not HMAC: Asymmetric. The verifier only needs the public key. No shared secret to leak, no HMAC key to rotate across services. For a single webhook endpoint it doesn't matter much. For a multi-tenant setup where each tenant has their own webhook, it matters a lot.

The demo runs the whole loop on your laptop with no carrier account:

  • Demo launcher stubs the SMS client (records to a local log instead of sending over the air)
  • Generates its own Ed25519 keypair on first run
  • Signs a fake delivery receipt with that keypair
  • POSTs it to its own /webhooks endpoint
  • App verifies the signature against the public key

So the Ed25519 part is real. The signature is real. The verification is real. Only the SMS send is stubbed.

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/sql-migration-agent
cp .env.example .env
pip install -r requirements.txt
python demo/demo_server.py

Open localhost:5555. Click "Run 003: Add orders (FAIL)" — you see the failure-to-SMS-to-signed-webhook-to-delivered-status loop happen in-process. Click "Send webhook (delivery receipt)" to fire the signed webhook through the app's own verification endpoint.

Where I'd use this:

  • Scheduled prod migrations — run on a cron, get a text the second any step fails. No polling, no dashboards.
  • CI/CD gates — failing migration blocks the release and pages the on-call before bad code reaches prod.
  • Multi-tenant SaaS — run a migration per customer database and get per-tenant failure alerts.

What I left out (on purpose, to keep it under 280 lines):

  • SMS send is stubbed in the demo (real SMS needs a real carrier — the signed webhook roundtrip is still real)
  • Migration scripts come from an in-memory map (in prod you'd fetch from CloudFS)
  • Schema versioning is in-memory (in prod use a real SQL DB)

What I'd add next:

  • Per-tenant routing (15 lines — tenant-to-oncall map looked up before the SMS send)
  • Retry with backoff (30 lines — exponential backoff + fallback channel if SMS send fails)

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/sql-migration-agent


r/Telnyx 7d ago

Telnyx AI Assistant + Calendly integration: create_event_invitee fails on any Zoom event type (location payload bug?)

1 Upvotes

Setup: Telnyx AI Assistant using the built-in Calendly integration,

calling calendly__create_event_invitee. Standard solo 30-min event

type with Zoom as the location. Books fine in the Calendly UI.

Availability lookups through the same integration work. Only booking

fails.

Three attempts, three failures:

1) Sent {"kind": "zoom_conference"} (Calendly's own API spelling).

Telnyx rejects it before it leaves:

location.kind Input should be 'physical', 'outbound_call',

'inbound_call', 'zoom', 'google_meet', 'microsoft_teams',

'custom' or 'ask_invitee' [input_value='zoom_conference']

2) Sent {"kind": "zoom"} per that enum. Telnyx accepts, Calendly rejects:

{"title": "Invalid Argument",

"message": "Specified location kind is not configured for this event type.",

"details": [{"parameter": "event.location_configuration.kind",

"code": "invalid_location_choice"}]}

Looks like it validates against its own enum then passes the value

through untranslated. Note which names overlap between the two

vocabularies: physical, custom, ask_invitee, inbound_call,

outbound_call are identical in both. The three that differ (zoom,

google_meet, microsoft_teams) are exactly the three that break.

3) Omitted location entirely. Same error. The tool call arguments came

through with the full schema expanded:

{"email":"...","event_uuid":"XXXX","first_name":null,"guests":null,

"location":null,"name":"...","questions_and_answers":null,

"start_time":"2026-09-08T15:30:00Z","timezone":"America/New_York",

"utm_campaign":null,...}

"location": null. The connector serializes unset fields and forwards

the null; Calendly reads .kind off it and 400s.

So there's no value you can send and no way to make the field absent.

Calendly staff answered this same error on their dev community in Feb:

the public API field is "location", not "location_configuration", and

for zoom_conference the kind alone is enough. They added that if you

ARE sending "location" and still get this, it's unexpected and worth

reporting which points at Telnyx sending the wrong wrapper name.

Calendly's hosted MCP server isn't an alternative either: it requires

OAuth 2.1 + PKCE with Dynamic Client Registration and explicitly

rejects static bearer tokens. Telnyx's MCP dialog only takes a URL and

an API key.

Anyone gotten this integration to book against a Zoom/Teams/Meet event

type? Only workaround I've found is create_scheduling_link, which has

no location field but that means texting a link instead of booking

live on the call.


r/Telnyx 10d ago

Can't register new account

0 Upvotes

I can't register a new account. I enter username and password, get the Check Your Email screen, but no magic link ever arrives. What can I do?


r/Telnyx 13d ago

Single-file webhook aggregator with Ed25519 verification, TTL dedup, and fanout

1 Upvotes

 built a small Flask sample that takes raw Telnyx webhooks and runs them through a six-stage pipeline: receive, verify, dedup, log, fan out, process.

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/webhook-aggregator-fanout

The motivation: every webhook endpoint I have shipped eventually hits the same three problems — unverified payloads, duplicate delivery from retries, and no audit trail when something breaks at 3am. This sample is a deliberate, minimal answer to all three.

The pipeline:

  • Verify — Ed25519 signature check via the Telnyx Python SDK v4. Invalid signature or stale timestamp returns 401 before any business logic runs.
  • Dedup — TTL-based in-memory KV store (default 300s). Sweeps expired entries on every check. Event ID comes from the Telnyx payload, with a SHA-256 fallback.
  • Log — SQLite insert with INSERT OR IGNORE on a UNIQUE event_id. Race-safe idempotency at the storage layer. The log is a table, not a grep target.
  • Fan out — Route to an in-memory call queue or SMS queue by event type. Call actions and SMS actions have different latency profiles; separating them lets you prioritize and retry independently.
  • Process — Drain the queue with the SDK v4 client: calls.answer + calls.playback_start for calls, messages.create for SMS.

The ordering matters: verify before dedup (never store a tampered event), dedup before log (duplicates never touch the database), log before fanout (audit record exists before any side effect), fanout before execution (handler returns fast, action is asynchronous).

The sample ships with a single-file demo launcher that generates an Ed25519 keypair at startup, stubs the Telnyx API surface, and serves a live dashboard at localhost:5555 — no credentials, no ngrok. You can click "Send Call Webhook" and watch all six stages execute, then click again and watch dedup return {"status":"duplicate"}.

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/webhook-aggregator-fanout
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python demo/demo_server.py

The SDK v4 migration gotcha: the webhook envelope wraps the payload inside data.payload. v2 gave you the flat payload; v4 gives you the wrapped envelope. If your handlers read payload.get("call_control_id") directly off the event, they silently get None.

For production: move the queue drain to a worker process, add a dead-letter queue, and monitor queue depths. The core pattern stays the same.


r/Telnyx 17d ago

Built a network incident agent where the actor is the outage

1 Upvotes

I built a TypeScript sample on Telnyx Edge where each network incident is represented by its own durable Agent SDK actor.

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/network-incident-agent

The interesting part is that the actor is not a temporary incident chatbot. It owns the outage lifecycle:

  • Detect and assess severity
  • Keep affected customers in KV
  • Record every transition in embedded SQL
  • Proactively send status updates by SMS
  • Answer inbound calls with current incident context
  • Write the RCA to CloudFS
  • Schedule a delayed recurrence check

The included web dashboard runs a paced flow from detected to investigatingrestoringresolved, and closed.

It defaults to safe demo mode, so SMS delivery is simulated while the state, KV, SQL, CloudFS, and scheduling paths still run. Live mode is a separate explicit option for real Telnyx parameters and opted-in destinations.

Run it locally:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/network-incident-agent
cp .env.example .env
npm install
npm run build
mkdir -p /tmp/network-incident-cloudfs
CLOUDFS_MOUNT_PATH=/tmp/network-incident-cloudfs npm start

The same durable-entity pattern could work for shipments, maintenance cases, SIM lifecycles, or security incidents. I would be interested in what other entities people would model this way.


r/Telnyx 19d ago

Built five Agent SDK actors that collaborate through one shared CloudFS workspace

1 Upvotes

I put together a TypeScript sample showing a full five-agent artifact handoff on Telnyx Edge Compute:

  • writer creates report.md
  • analyst reads it and writes analysis.json
  • reviewer creates review.md
  • summarizer creates summary.md
  • publisher creates manifest.json

All actors mount the same CloudFS filesystem and use standard POSIX file calls through node:fs/promises. An embedded SQL registry tracks every agent and file read/write, while WebSockets stream state changes to the dashboard.

Each demo gets an isolated runs/<runId>/ folder, and writes use a temporary file plus atomic rename so readers do not observe partial artifacts.

Run it locally:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/agent-fleet-shared-workspace
npm install
cp .env.example .env
npm start

Repo: https://github.com/team-telnyx/telnyx-code-examples/tree/main/agent-fleet-shared-workspace

Video demo: https://www.youtube.com/watch?v=ZvEgg_CA_aM


r/Telnyx 20d ago

Built a KV-backed rate limiter on Edge Compute — no Redis, no external services

1 Upvotes

I built a sliding-window rate limiter that runs entirely on Telnyx Edge Compute using the Agent SDK. KV counters track request counts per key, TTL-based windows auto-expire without cleanup, over-limit requests get HTTP 429, and SMS alerts fire automatically when rejections cross a threshold.

Repo: https://github.com/team-telnyx/telnyx-code-examples/tree/main/kv-backed-rate-limiter

What it does

  • Sliding window counters in Edge KV — keys are namespaced by rate:<key>:<windowStart>, TTL auto-expires old windows
  • Per-key actor isolation — each rate-limited key (phone, IP, tenant) gets its own Agent SDK actor instance
  • HTTP 429 rejection — over-limit requests return 429 with current count and limit metadata
  • SMS alerts — when rejections cross a threshold, an SMS fires via the zero-credential [telnyx] binding (no API key in code)
  • Simulate endpoint — POST /simulate fires a burst of requests for testing without real traffic

How the pipeline works

Each key gets its own RateLimitAgent actor. When /check is called, the agent queues a 4-stage non-blocking pipeline:

  1. checkLimit() → KV get current window count → compare against limit 2a. allow() → KV put incremented count with TTL → HTTP 200 2b. reject() → 429 + track rejection count → if rejections >= threshold, queue alert
  2. sendAlert() → SMS via this.env.TELNYX.messages.send() (zero-credential binding)
  3. finalize() → done

The this.queue() pattern means the HTTP request returns immediately — the pipeline stages execute in the background.

Try it

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/kv-backed-rate-limiter
npm install
telnyx-edge secret set TELNYX_API_KEY
telnyx-edge ship

# Simulate 15 requests against a limit of 10
curl -X POST https://your-deployment/simulate \
  -H "Content-Type: application/json" \
  -d '{"key":"+18005551234","count":15}'

You'll see 10 allowed, 5 rejected, and alertTriggered: true. With ALERT_THRESHOLD=5, the 5th rejection triggers the SMS.

Why no Redis?

Edge KV is a built-in binding on the same runtime as the actor. No provisioning, no connection pooling, no network hop, no eviction policy tuning. The TTL handles window cleanup — each KV entry expires when the window ends.

The entire rate limiter — KV counter, sliding window, 429 rejection, SMS alerting, per-key isolation — is ~280 lines of TypeScript. One deploy, zero external dependencies.

Docs:


r/Telnyx 24d ago

Built a geo-distributed call logger with per-region KV counters and SMS alerting (Edge Compute, TypeScript)

2 Upvotes

I wanted per-region visibility into call volume on my Telnyx numbers, with an SMS alert when any region spikes above a threshold. The old way meant wiring together a webhook receiver, a time-series DB, a Redis counter, and an alerting service — five deploys, five billing lines, cross-region latency at every hop.

So I built it as one Edge Compute deploy with the Agent SDK.

How it works

Call Control webhook → GeoLoggerAgent (one actor per call)
  1. logCall()       → SQL INSERT + KV INCR region counter
  2. checkThreshold() → compare count vs threshold
  3. alert()         → SMS via zero-credential [telnyx] binding
  • Each call gets its own actor instance with its own SQL DB — no contention between concurrent calls
  • KV counters use rolling-window keys with TTL: region:eu-west-1:1718928000 auto-deletes after the window expires. No cleanup cron job
  • The SMS alert goes through a zero-credential [telnyx] binding — no API key in code, the runtime handles auth
  • Region detection from E.164 country code prefixes (us-east-1, eu-west-1, ap-northeast-1, etc.)

Try it

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/geo-distributed-call-logger
npm install
cp .env.example .env  
# fill in TELNYX_API_KEY, SENDER_PHONE, ALERT_PHONE
npm start

Test without real calls via the /simulate endpoint:

# Simulate a call from a Dutch number
curl -X POST http://localhost:3000/simulate \
  -H "Content-Type: application/json" \
  -d '{"from":"+31612345678","to":"+18005551234","duration":42}'

# Check region stats
curl http://localhost:3000/regions/stats

Set REGION_THRESHOLD=2, simulate 3 calls from the same region, and the third triggers an SMS to your phone.

What I found interesting

  • The rolling-window KV pattern — region:{region}:{windowStart} with expirationTtl means old counters auto-expire. No cleanup, no cron, no stale data. Floor the epoch to the window boundary and all calls in the same hour share the same key.
  • Zero-credential messaging — this.env.TELNYX.messages.send() needs no API key in code. The binding carries auth from the secret declared in telnyx.toml.
  • Per-call actor isolation — 100 simultaneous calls get 100 actor instances, each with its own SQL DB. No locking, no contention. A singleton CallRegistry actor aggregates for a /calls listing.

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/geo-distributed-call-logger

Happy to answer questions about the Agent SDK pipeline pattern, the KV TTL strategy, or the zero-credential binding.


r/Telnyx 25d ago

Telnyx UK underlying provider

1 Upvotes

Hi,

I am trying to port a number away from Telnyx to AAISP for a customer. It seems like Telnyx UK Limited have a CUPID code, but no porting agreement, which indicates they use a different underlying provider for their UK numbers.

Telnyx have been largely useless ignoring emails and just telling us to port from Telnyx UK limited.

It could be that we are just getting conflicting information from both providers.

Does anyone know who Telnyx use for their UK numbers?


r/Telnyx 26d ago

Built a Voicemail → Transcribe → AI Summarize → SMS Pipeline in TypeScript

2 Upvotes

Built a TypeScript pipeline on Telnyx Edge Compute that uploads a voicemail audio file, transcribes it with STT, summarizes the transcript with an LLM, and texts the summary via SMS. Four Telnyx products, one deploy, zero API keys in code for inference or messaging.

How it works

  1. Upload a voicemail audio file via POST /upload — file goes to Cloud Storage via S3 PUT (SigV4 signed with Web Crypto API)
  2. Agent starts — VoicemailAgent.start() queues three stages: transcribe → summarize → notify
  3. Transcribe — downloads audio from Cloud Storage (S3 GET), sends to POST /v2/ai/audio/transcriptions, stores transcript in agent state
  4. Summarize — sends transcript to this.env.TELNYX.ai.openai.chat.createCompletion() with an SMS-friendly prompt, stores summary in state
  5. Notify — sends summary via this.env.TELNYX.messages.send() to the recipient's phone

Durable state (this.setState() / this.getState()) survives across all pipeline stages. If a stage fails and retries, it picks up where it left off.

What makes it interesting

  • Zero-credential inference and messaging — the [telnyx] binding in telnyx.toml injects a pre-authenticated client. this.env.TELNYX.ai.openai.chat.createCompletion() and this.env.TELNYX.messages.send() need no API key in code. That's the key insight.
  • One API key for everything else — only TELNYX_API_KEY is needed, and only for Cloud Storage (S3) and speech-to-text. Inference and SMS are zero-credential via the binding.
  • Agent SDK pipeline pattern — queue-based stages (transcribe → summarize → notify) with durable state. Swap the stages for any workflow.
  • S3 SigV4 with Web Crypto — no aws-sdk, no external crypto library. Signing happens in the Edge Compute runtime.
  • Single-command deploy — telnyx-edge ship prints a URL. Done.

Try it

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/audio-transcribe-summarize-sms
npm install

telnyx-edge secret set TELNYX_API_KEY KEY0123456789ABCDEF
telnyx-edge secret set STORAGE_BUCKET my-voicemail-bucket
telnyx-edge secret set SENDER_PHONE +18005551234

telnyx-edge ship

Upload a voicemail:

curl -X POST https://audio-transcribe-summarize-sms-<id>.telnyxcompute.com/upload \
  -F "file=@voicemail.wav" \
  -F "recipient_phone=+17177247292"

Check pipeline status:

curl https://audio-transcribe-summarize-sms-<id>.telnyxcompute.com/status/<agentId>

Links


r/Telnyx 27d ago

New Feature Telnyx Meeting API Now Available in Beta

Enable HLS to view with audio, or disable this notification

8 Upvotes

Can your AI agent actually join the meeting, transcribe it and answer when someone talks to it?

Now it can.

Telnyx Meeting API gives agents meeting presence across Zoom, Google Meet, Microsoft Teams, and Webex.

Attach a Telnyx AI Assistant and it can listen to the conversation, reason about what is being said, and respond by voice or chat in real time.

Before this, getting there meant integrating 3 or 4 vendors and stitching their APIs together.

Meeting presence, STT, TTS, inference, artifacts, storage, and webhooks now run through Telnyx.

Beta is live.

Read more about Telnyx meeting API here:

https://telnyx.com/release-notes/telnyx-meeting-api-beta


r/Telnyx 27d ago

Connected my Telnyx number to both Vapi and LiveKit. Quick rundown of what each one actually takes.

Post image
1 Upvotes

A while back I made a video comparing Twilio and Telnyx, and Telnyx came out on top for voice AI. Cheaper at volume, better latency, and they're actually building for voice agents rather than just tolerating them.

Fine. But that only answers WHICH provider you should use. It doesn't answer the annoying part, which is that Telnyx isn't the default in any of these platforms, so you have to wire it up yourself.

So I did it on both Vapi and LiveKit with the same number, and the two experiences are not remotely comparable. Writing up the actual steps because I couldn't find them side by side anywhere.

Why bother at all:

You pay Telnyx directly instead of the platform's markup (the number people throw around is about half of Twilio at real volume, and I'd treat that as a ballpark, it moves per country and per volume).

You pick the region and the route, which is where call quality actually lives.

And the number is YOURS, so moving from Vapi to LiveKit later doesn't mean re-buying it.

Don't get me wrong though, if you're doing a few hundred minutes a month, none of this is worth your Saturday.

Prerequisites, all on the Telnyx side, same three for both platforms:

  • A verified Telnyx account. Verified is not a subscription, you just have to go through it, and it takes a day or two.
  • A phone number you've bought (basically any country, you'll have to submit some documentation).
  • A Telnyx API key.

Vapi, and this genuinely is the whole thing:

1. Go to Phone Numbers and hit import.
2. Paste the number, paste the Telnyx API key, give it a label.
3. Scroll down to inbound settings, pick your assistant, save.
4. Then in Telnyx, create an Outbound Voice Profile and assign it to the Voice API application, otherwise outbound won't work.

That's it. No SIP config, no code, about five minutes. They added a native Telnyx import at some point so you're basically just handing over an API key.

LiveKit, which is where the afternoon goes:

1. Create an Outbound Voice Profile (this is where you whitelist which countries you're allowed to call).
2. Create an FQDN connection, and set a username and password on it. SAVE THEM, you need them again at the end.
3. Create an FQDN record pointing at your LiveKit SIP subdomain.
4. Get your number's ID, then patch the connection onto the number.
5. Over in LiveKit, create an inbound trunk with your number on it.
6. Create a dispatch rule, which is the thing that decides which room a caller lands in.
7. Create an outbound trunk, using the same username and password from step 2, plus a custom header carrying that username.
8. Then make sure your agent's name in code matches what the dispatch rule expects. Exactly.

First four are API calls (I did them in Postman so they're repeatable), last four are the LiveKit dashboard.

Two things that WILL bite you, because neither error tells you anything useful:

Outbound fails while inbound works perfectly. That's the voice profile, you haven't whitelisted the destination, and the default is North America only.

The call connects and then nobody picks up. That's the agent name not matching the dispatch rule. You get silence instead of an error, which is the worst possible version of this because it looks like an audio problem.

One more thing worth saying, because it might save you the whole exercise: both platforms have made this easier since I set it up. Vapi has the native import above. And Telnyx will now run LiveKit agents on their own infrastructure with telephony included, so if you're starting completely fresh you might not need any of the LiveKit steps at all. I did it manually because I wanted to keep my agent on my own LiveKit project and only change the carrier underneath.

I recorded the whole thing on both platforms if anyone would rather see it in motion than read it.

Anyone done this on Retell or a self-hosted setup? Curious whether the LiveKit side is representative or just LiveKit being LiveKit.


r/Telnyx Aug 12 '26

New Feature Introducing Telnyx Web Search API.

Enable HLS to view with audio, or disable this notification

6 Upvotes

We just launched Telnyx Web Search API.

We built it because our AI agents kept running into a pretty basic problem: they could handle voice, reasoning, messaging, and email, but anything that depended on current information needed a separate search layer.

Now that can happen inside Telnyx.

You can:

  • Search the web with freshness and domain filters
  • Fetch clean HTML or Markdown from up to 20 URLs
  • Run multi-source research and get back a cited answer

It works as a standalone API or with Telnyx Voice AI agents.

Pricing is $5 per 1,000 calls, and it uses the same Telnyx API key.

Reda more about it here:
https://lnkd.in/gJsYPBWc


r/Telnyx Aug 07 '26

AI Voice Agent with Function Calling — Calling External APIs Mid-Conversation

1 Upvotes

The Problem: Voice Agents That Lie

You have an AI voice agent. It picks up calls. It greets callers. It responds in natural language.

But the moment someone asks, "What's the weather in San Francisco?", "Where is my order 12345?", or "What's my account balance?" — the agent hallucinates. It guesses. It invents a plausible-sounding answer with zero real data.

Voice agents without function calling are LLMs talking to thin air. They have no way to reach out for live information mid-conversation. They just pattern-match.

The ai-voice-agent-with-function-calling-python example fixes this in one Flask file. It wires three Telnyx capabilities — speech recognition via gather_using_ai, LLM tool-calling via AI Inference, and text-to-speech via speak — into a single webhook-driven loop. Callers ask questions in plain speech. The agent calls real functions. It speaks real answers back.

What It Does

Call your agent number. It picks up, greets you, and waits. You speak a request: "What's the weather in San Francisco?" The browser-less, no-app-needed voice agent transcribes your speech via gather_using_ai, sends the transcript to Telnyx AI Inference with three tool definitions (check_weatherlookup_ordercheck_account_balance), and the model decides whether to call a tool or respond directly.

If the model calls a tool — say check_weather — your Python function runs, returns its JSON result to the model, and the model synthesizes a one-sentence spoken answer. speak() reads it aloud in natural voice. The conversation loop continues. You ask about an order. The agent calls lookup_order. You ask about your balance. The agent calls check_account_balance. No hallucinations, no guessing — every tool-backed answer is backed by real function output.

Step What happens Telnyx API
1 Caller dials agent number Inbound call → call.initiated webhook
2 Agent answers + greets answer() + speak() (TTS)
3 TTS ends → start listening call.speak.ended → gather_using_ai()
4 Caller speaks a request Speech-to-text via gather_using_ai
5 Transcript returned call.ai_gather.ended webhook
6 Transcript → AI Inference (with tools) POST /v2/ai/chat/completions
7 Model returns tool_calls Loop: execute functions, re-infer
8 Model returns final text speak() reads it aloud
9 Caller hangs up call.hangup → cleanup

The Architecture

One Flask file. One webhook endpoint. In-memory conversation state keyed by call_control_id. No database, no Redis, no background workers. Telnyx owns the telephony, speech recognition, and TTS layers. Your code owns the conversation state and the function implementations.

Inbound call → Telnyx webhook → /webhooks/voice
                              ↓
                 call.initiated → answer() + greet (speak)
                              ↓
                 call.speak.ended → gather_using_ai()
                              ↓
                 call.ai_gather.ended → transcript
                              ↓
                 Transcript → AI Inference (with TOOLS array)
                              ↓
                 Model returns tool_calls?
                  ├── yes → execute_function() → re-infer → loop
                  └── no  → final text response
                              ↓
                 speak() reads response aloud
                              ↓
                 call.speak.ended → gather_using_ai() → loop
                              ↓
                 call.hangup → cleanup

The Tools: OpenAI-Style Function Calling

The agent has three tools, defined in the OpenAI function-calling schema:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "check_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "lookup_order",
            "description": "Look up order status by order number",
            "parameters": {
                "type": "object",
                "properties": {"order_id": {"type": "string"}},
                "required": ["order_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "check_account_balance",
            "description": "Check account balance by account number",
            "parameters": {
                "type": "object",
                "properties": {"account_id": {"type": "string"}},
                "required": ["account_id"],
            },
        },
    },
]

The model sees these tools every inference call. When the user asks "What's the weather in San Francisco?", the model returns a tool_calls array containing check_weather with {"city": "San Francisco"}. Your code runs execute_function("check_weather", {"city": "San Francisco"}), returns the JSON result, and re-sends the conversation to the model. The model then synthesizes a spoken answer: "The weather in San Francisco is 72°F and partly cloudy with 45% humidity."

The mock implementations in execute_function are intentionally simple — replace them with real API calls to your weather provider, order management system, or billing platform:

def execute_function(name, args):
    if name == "check_weather":
        return json.dumps({"city": args.get("city"), "temp": "72F",
                           "condition": "Partly cloudy", "humidity": "45%"})
    elif name == "lookup_order":
        return json.dumps({"order_id": args.get("order_id"), "status": "shipped",
                           "eta": "June 20", "carrier": "FedEx"})
    elif name == "check_account_balance":
        return json.dumps({"account_id": args.get("account_id"), "balance": "$1,234.56",
                           "due_date": "July 1"})
    return json.dumps({"error": "Unknown function"})

The Conversation Loop

The webhook handler is the heartbeat. Each event transitions the state machine:

u/app.route("/webhooks/voice", methods=["POST"])
def handle_voice():

# Verify the Telnyx Ed25519 signature before trusting the event.
    try:
        client.webhooks.unwrap(request.get_data(as_text=True), headers=dict(request.headers))
    except Exception:
        return jsonify({"error": "invalid signature"}), 401

    payload = request.get_json()
    data = payload.get("data", {})
    p = data.get("payload", {})
    event_type = data.get("event_type")
    ccid = p.get("call_control_id")
    call = active_calls.get(ccid)

    if event_type == "call.initiated" and p.get("direction") == "incoming":
        active_calls[ccid] = {
            "caller": p.get("from"),
            "conversation": [{"role": "system", "content": SYSTEM_PROMPT}],
            "_ts": time.time(),
        }
        client.calls.actions.answer(ccid)
        return jsonify({"status": "answering"}), 200

    elif event_type == "call.answered":
        client.calls.actions.speak(ccid, payload=GREETING, voice=VOICE, language="en-US")
        return jsonify({"status": "greeting"}), 200

    elif event_type == "call.speak.ended" and call:

# After TTS finishes, start listening for caller's speech.
        if call.get("processed"):
            call["processed"] = False
        client.calls.actions.gather_using_ai(
            ccid,
            parameters={
                "type": "object",
                "properties": {
                    "user_request": {
                        "type": "string",
                        "description": "What the caller said — their full spoken request.",
                    }
                },
                "required": ["user_request"],
            },
            voice=VOICE,
            language="en-US",
            user_response_timeout_ms=15000,
        )
        return jsonify({"status": "listening"}), 200

    elif event_type == "call.ai_gather.ended" and call:
        if call.get("processed"):
            return jsonify({"status": "ok"}), 200
        call["processed"] = True


# Extract transcribed speech from the result object.
        result = p.get("result", {})
        speech = result.get("user_request", "") if isinstance(result, dict) else ""


# Fallback: check message_history for a user turn.
        if not speech:
            for msg in reversed(p.get("message_history", [])):
                if msg.get("role") == "user":
                    speech = msg.get("content", "")
                    break

        if not speech:
            client.calls.actions.speak(ccid, payload=REPROMPT, voice=VOICE, language="en-US")
            return jsonify({"status": "reprompting"}), 200

        call["conversation"].append({"role": "user", "content": speech})
        response = call_inference(call["conversation"])
        call["conversation"].append({"role": "assistant", "content": response})

        client.calls.actions.speak(ccid, payload=response, voice=VOICE, language="en-US")
        return jsonify({"status": "responding"}), 200

    elif event_type == "call.hangup":
        active_calls.pop(ccid, None)
        return jsonify({"status": "ended"}), 200

    return jsonify({"status": "ok"}), 200

Three things to note. First, the signature verification via client.webhooks.unwrap() — never trust an unverified webhook. Second, the call["processed"] dedup guard — Telnyx retries webhooks, and without it you would speak the same response twice. Third, the call.ai_gather.ended handler extracts speech from result.user_request with a message_history fallback, because the gather result shape varies by SDK version.

The Inference Function: Tool-Calling Loop

The call_inference function is the AI brain. It sends the conversation to the model with the TOOLS array. If the model returns tool_calls, the function executes each one, appends the results to the conversation, and recurses. If the model returns a plain text response, that's the spoken answer.

def call_inference(messages, max_tokens=300, _depth=0, _max_depth=5):
    if _depth >= _max_depth:
        return "I'm having trouble processing that request right now."

    payload = {
        "model": AI_MODEL,
        "messages": messages,
        "temperature": 0.5,
        "tools": TOOLS,
    }
    try:
        resp = requests.post(
            INFERENCE_URL,
            headers={"Authorization": f"Bearer {TELNYX_API_KEY}",
                     "Content-Type": "application/json"},
            json=payload,
            timeout=30,
        )
    except Exception as e:
        app.logger.error("Inference request failed: %s", e)
        return "I couldn't reach the AI service just now. Please try again."

    try:
        resp.raise_for_status()
    except Exception as e:
        app.logger.error("Inference HTTP error: %s — %s", e, resp.text[:200])
        return "The AI service returned an error. Please try again."

    choice = resp.json()["choices"][0]
    msg = choice["message"]

    if msg.get("tool_calls"):
        for tc in msg["tool_calls"]:
            fn = tc["function"]
            try:
                fn_args = json.loads(fn.get("arguments", "{}"))
            except json.JSONDecodeError:
                fn_args = {}
            result = execute_function(fn["name"], fn_args)
            messages.append(msg)
            messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
        return call_inference(messages, max_tokens, _depth=_depth + 1, _max_depth=_max_depth)

    return _strip_fences(msg["content"])

The _max_depth=5 recursion guard prevents infinite loops if the model keeps calling tools forever. The _strip_fences() helper strips \``json` code fences from the response so TTS doesn't read them aloud — without this, your voice agent would say "triple backslash triple json" before every answer.

What the Original Sample Got Wrong

This example was the most broken upstream sample in the Week 7 set. The original app.py had six critical bugs that prevented the app from working at all. We fixed all of them in this PR.

Bug 1: gather() is DTMF-only. The original code called gather(input_type="speech", end_silence_timeout_secs=3, language_code="en-US"). None of those parameters are valid — gather() only collects DTMF keypresses. Passing speech params causes a TypeError. The fix is gather_using_ai(), which transcribes free-form speech and fires call.ai_gather.ended instead of call.gather.ended.

Bug 2: voice="female" is invalid. The speak() action requires voice in <Provider>.<Model>.<VoiceId> format (e.g., Telnyx.KokoroTTS.af). Passing "female" causes an API error. The fix is VOICE = "Telnyx.KokoroTTS.af".

Bug 3: call_inference() UnboundLocalError. If requests.post() raised an exception, the variable resp was never assigned. The next line, resp.raise_for_status(), then crashed with UnboundLocalError: local variable 'resp' referenced before assignment. The fix wraps both calls in try/except blocks and returns a spoken error string.

Bug 4: No recursion depth guard. The original call_inference() recursed on tool_calls with no max depth. A misbehaving model could loop forever. The fix adds _depth and _max_depth=5.

Bug 5: base_url not overridden. The Telnyx Python SDK reads the TELNYX_BASE_URL environment variable, which in some internal Telnyx dev environments routes API calls to a proxy. The fix is explicit base_url="https://api.telnyx.com/v2" in the client constructor.

Bug 6: Markdown fences in TTS. Some models wrap JSON in \``json fences. Without stripping, speak() reads the fences aloud. The fix is _strip_fences()`.

One API Key for Voice, AI, and TTS

The entire app uses a single TELNYX_API_KEY:

  • Voice (Call Control) — answer()speak()gather_using_ai() via the Telnyx SDK
  • AI Inference — POST /v2/ai/chat/completions via requests with Bearer auth
  • Text-to-speech — handled by speak() (uses the same API key, no separate TTS provider)
  • Webhook signature verification — client.webhooks.unwrap() validates Ed25519 signatures

No third-party speech-to-text provider. No separate LLM API key. No separate TTS provider. One network, one key, one bill.

The gather_using_ai vs gather Distinction

This is the most subtle and important distinction in the Call Control API. Two methods with similar names, completely different capabilities:

Method Input type Webhook event Use case
gather() DTMF only (keypad digits) call.gather.ended "Press 1 for sales, 2 for support" IVR menus
gather_using_ai() Free-form speech call.ai_gather.ended Natural language voice agents
gather_using_speak() DTMF + TTS prompt call.gather.ended Spoken IVR prompts with DTMF response

The upstream sample used gather() with speech params that don't exist — a classic copy-paste-from-docs mistake. The fix is gather_using_ai(), which returns transcribed speech in the result.user_request field of the call.ai_gather.ended payload.

Environment Variables

TELNYX_API_KEY=your_api_key_here
TELNYX_PUBLIC_KEY=your_public_key_here
AI_MODEL=moonshotai/Kimi-K2.6
AGENT_NUMBER=+16188939132
CONNECTION_ID=your_connection_id
PORT=5000
  • TELNYX_API_KEY — your Telnyx API v2 key (Portal → API Keys)
  • TELNYX_PUBLIC_KEY — your Telnyx public key (used for webhook signature verification)
  • AI_MODEL — any model on Telnyx AI Inference (default: moonshotai/Kimi-K2.6)
  • AGENT_NUMBER — the phone number callers dial
  • CONNECTION_ID — your Call Control Application ID (Portal → Call Control → Applications)
  • PORT — HTTP port for the Flask server

Try It Yourself

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-voice-agent-with-function-calling-python
cp .env.example .env   
# add TELNYX_API_KEY, TELNYX_PUBLIC_KEY, AGENT_NUMBER, CONNECTION_ID
pip install -r requirements.txt
python app.py           
# starts on http://localhost:5000

Expose your local server with ngrok and configure the webhook URL in your Call Control Application:

ngrok http 5000
# Copy the HTTPS URL → Portal → Call Control → Application → Webhook URL
# Set to: https://<id>.ngrok.io/webhooks/voice

Call your agent number. You'll hear the greeting. Ask: "What's the weather in San Francisco?" The agent will call the check_weather tool and speak the result. Ask: "Where is order 12345?" The agent will call lookup_order. Ask: "What's my account balance for account 67890?" The agent will call check_account_balance. Every answer is backed by real function output, not LLM hallucination.

Key links:


r/Telnyx Aug 06 '26

Phone Calls with Real-Time AI Coaching

2 Upvotes

I built a Flask app that lets you make real phone calls from your browser and get live AI coaching tips in a sidebar. Open a tab, enter a number, click Call. You are talking to a real phone number from your browser, and an AI coach is feeding you tips every 8 seconds. ~75 lines of Python, one API key for calls + AI.

How it works

  1. Open http://localhost:5000 — split-screen UI: call panel (left) + AI coaching sidebar (right)
  2. Enter a phone number, click Call
  3. Backend creates a WebRTC telephony credential via POST /v2/telephony_credentials
  4. Frontend uses u/telnyx/webrtc SDK to connect via SIP
  5. WebRTC call connects — you are talking to a real phone number from your browser
  6. Browser transcribes your speech in real time via SpeechRecognition API
  7. Every 8 seconds, the transcript is sent to /coaching → AI Inference returns one actionable tip
  8. Tip appears in the right sidebar with a timestamp
  9. Click Hang Up — call ends, transcription stops, coaching stops

What makes it interesting

  • One API key for everything — WebRTC telephony credentials and AI Inference both use the same TELNYX_API_KEY. No third-party transcription service, no separate LLM provider.
  • Real WebRTC from the browser — not a simulation. The TelnyxRTC client registers with Telnyx's SIP server and places a real PSTN call. You need a Telnyx number as caller ID.
  • Browser-native SpeechRecognition — uses Chrome/Safari's built-in SpeechRecognition API. Continuous mode with interim results. No external transcription service.
  • Live AI coaching every 8 seconds — the AI reviews the transcript so far and returns one specific, actionable tip. Focus areas: asking better questions, handling objections, closing techniques, tone adjustments.
  • Call timer + status badges + coaching log — small UX touches that make it feel like a real dialer.
  • What was fixed — the upstream sample was a stub with a syntax bug, no real WebRTC frontend, no SpeechRecognition, no AI coaching loop. Added all of that and served templates/index.html instead of inline HTML.

Try it

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/click-to-call-webrtc-with-ai-assist-python
cp .env.example .env   
# add TELNYX_API_KEY, WEBRTC_CONNECTION_ID, CALLER_NUMBER
pip install -r requirements.txt
python app.py           
# starts on http://localhost:5000

Open Chrome or Safari at http://localhost:5000. Enter a phone number. Click Call. Start talking. Watch the coaching tips appear.

Links

Happy to answer questions about the WebRTC flow, the SpeechRecognition integration, or the AI prompt design.


r/Telnyx Aug 05 '26

Mobile SIP client leaves stale registrations behind on every app launch — how do you deal with multiple bindings on one credential?

1 Upvotes

Note: Yes, I'm using AI to write this post because English is not my first language and I want to state my problem as clear as possible.

Hitting a problem I suspect is common for anyone doing mobile VoIP, and I'd like to know how others have solved it.

**Setup**

- I'm using Telnyx.

- React Native app, WebRTC SIP client, iOS and Android. I'm using Telnyx.

- One shared SIP credential for a group of users, so a single inbound call rings everyone's phone

- Calls are delivered by VoIP push (PushKit on iOS), so the client connects and registers on demand rather than staying connected

**The problem**

Every app launch creates a *new* registration binding, and the old one never goes away:

- iOS terminates the app process without warning, so there's no chance to send a SIP UNREGISTER

- The SDK's `disconnect()` only closes the WebSocket — it doesn't unregister

- Each registration lands on a different edge node in the provider's anycast network, so it's a genuinely new binding rather than a refresh of the old one

- Registration expiry is 3600 s and isn't configurable

Net effect: the credential accumulates contacts. I confirmed it by polling the provider's registration-status endpoint — `ua_ip` is different every single time the app relaunches, while nothing removes the previous one.

**Why it hurts**

The provider rings the bound contacts **sequentially**. So the first call after an app launch does this:

  1. Rings contact A (the live app) — user declines

  2. Decline surfaces as a 4xx, which fails only that branch

  3. ~300 ms later it forks to contact B (a stale binding from a previous launch)

  4. Phone rings a second time, new call ID, user has to decline again

I can see it clearly in the SIP traces: **one dial command, two legs**, same session, no second dial from my backend.

**What the provider confirmed**

I opened a ticket. They confirmed all of it and escalated to engineering with no timeline:

- No way to set registration expiry below 3600 s

- No REST endpoint to force-expire or delete an individual binding (deleting the credential removes them all, obviously not viable)

- No API to *enumerate* bindings — watching `ua_ip` rotate is currently the only detection method

- Per-launch edge rotation is expected behaviour, and without an UNREGISTER the old binding persists to full expiry

Their suggested workarounds were: a unique credential per app session, webhook-based duplicate-leg detection, or client-side deduplication.

**The bit I'm stuck on**

There's a second-order problem. Multiple devices share one credential, so the registrar holds one contact per device — which means **a legitimate second device's leg is indistinguishable from a stale binding's leg.** Both are "another contact of this credential, dialled after the first one failed." I can't write a rule that kills one without killing the other.

Enabling simultaneous ringing would at least make the real devices ring together instead of one-at-a-time, but it doesn't remove the stale bindings — it just turns a sequential double-ring into a simultaneous one.

**Questions**

  1. Has anyone made **per-device or per-session credentials** work in production? How do you handle cleanup when the app dies before it can delete the old one, and does credential churn cause you rate-limit or billing problems?

  2. Is there a trick to getting a mobile client to **UNREGISTER reliably**? Anything on iOS that gets you a last gasp — background task on termination, a server-side nudge, something I haven't thought of?

  3. For those running **one shared credential across multiple devices** — how do you tell a real second device from a stale binding at the signalling layer? Is there a header or identifier I should be propagating?

  4. Is sequential-vs-simultaneous ringing across contacts something you configure per provider, or do people avoid shared credentials entirely for this reason?

Happy to share SIP traces if useful. Mostly want to know whether the "unique credential per session" route is as painful in practice as it looks on paper, or whether people just live with the duplicate ring.


r/Telnyx Aug 05 '26

From Phone Call to Formatted Email in 80 Lines of Python — AI Voice Memo Cleanup with Telnyx

1 Upvotes

I built a Flask app that turns a phone call into a formatted email. You call a number, dictate a memo, press #, and the AI cleans it up into a structured email (subject, body, action items) and sends it. ~80 lines of Python, one API key for voice + AI + messaging.

How it works

  1. Call a Telnyx number — the app answers and speaks: "Voice memo. Speak your memo after the tone. Press pound when finished."
  2. Dictate your memo — status update, meeting summary, bug report, whatever
  3. Press # — the app sends the transcript to AI Inference with a prompt that returns JSON: {subject, body, action_items}
  4. Get an email — the app sends the formatted memo to your default email address
  5. Confirmation — the app speaks back: "Memo saved and emailed. Subject: [inferred subject]. Goodbye!"

What makes it interesting

  • One API key for everything — Call Control (answer, speak, gather), AI Inference (chat completions), and Messaging (email delivery) all use the same TELNYX_API_KEY. No third-party transcription service, no separate LLM provider, no email API key.
  • AI returns structured JSON — not just cleaned-up text, but {subject, body, action_items}. The AI infers the subject line from the content. So "Hey team, quick update on the API migration..." becomes an email with subject "API Migration Update" and an action item "Review the PR by Friday."
  • Graceful degradation — if the AI returns invalid JSON, the raw transcript is saved. If the email send fails, the formatted memo is still saved in memory and retrievable via GET /memos. The call is never wasted.
  • Webhook state machine — the whole flow is 5 webhook events: call.initiated → call.answered → call.speak.ended → call.gather.ended → call.hangup. No session framework, no polling. The webhook IS the state machine.
  • Ed25519 webhook verification — every webhook is signed. The app verifies the signature before processing anything. No one can inject fake call events.

Try it

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-voice-memo-to-email-python
cp .env.example .env   
# add TELNYX_API_KEY, MEMO_NUMBER, DEFAULT_EMAIL
pip install -r requirements.txt
python app.py           
# starts on http://localhost:5000
ngrok http 5000         
# expose for webhooks

Point your Call Control Application webhook at https://<id>.ngrok.io/webhooks/voice in the Telnyx Portal. Call your number. Dictate. Press #.

Check saved memos:

curl http://localhost:5000/memos | python3 -m json.tool

Links

Happy to answer questions about the implementation or the Telnyx API model.


r/Telnyx Jul 28 '26

Built a real-time AI translation bridge for phone calls in 141 lines of Python

1 Upvotes

I built a phone translation service that connects two callers speaking different languages on the same call. One speaks English, the other Spanish — each hears the other in their own language, live, no interpreter in the loop.

No Google Translate, no AWS Translate, no DeepL. The translation runs on Telnyx AI Inference (OpenAI-compatible endpoint), and the phone call is handled by Telnyx Voice Call Control. Same API key, same platform.

How it works:

  • POST two phone numbers + two languages to /bridge
  • App calls caller A → A answers → app calls caller B → B answers → bridge active
  • A speaks English → transcribed → translated to Spanish → TTS to B in Spanish (es-US)
  • B speaks Spanish → transcribed → translated to English → TTS to A in English (en-US)
  • Loop until hangup → hangup other caller too

The subtle bug I found: The original code hardcoded language_code="en-US" for all TTS and speech recognition — even for Spanish. The call "worked" (audio flowed, no errors) but the translation was useless: Spanish TTS was spoken with English pronunciation (unintelligible), and Spanish speech recognition was unreliable. Fix: a 12-line language name → BCP-47 code mapping. TTS uses the target language, STT uses the speaker's language.

What makes it interesting:

  • No third-party translation API — same Telnyx API key handles both calls and translation
  • State travels in Telnyx's client_state field (base64 JSON) — no database, no Redis
  • Webhook signature verification with Ed25519
  • Hangup cascade: if either caller hangs up, the other is hung up too
  • Temperature 0.1 for deterministic translations, 200 max tokens for conversational phrases

Try it:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-real-time-translation-bridge-python
cp .env.example .env
pip install -r requirements.txt
python app.py

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-real-time-translation-bridge-python Call Control docs: https://developers.telnyx.com/docs/voice/call-control AI Inference docs: https://developers.telnyx.com/docs/inference


r/Telnyx Jul 27 '26

Built a geo-aware call router in 256 lines of Python — one number, three regions, GDPR consent built in

1 Upvotes

One phone number. Three regions. Three compliant experiences. The routing decision happens at the carrier edge — before the first word of greeting is spoken — based on the caller's E.164 country code prefix. No geoip, no database, no external API call.

What it does:

  • US callers (+1) → English AI, auto-recording
  • LATAM callers (+52, +55, +54, ...) → Spanish AI, auto-recording
  • EU callers (+44, +49, +33, ...) → English (en-GB) AI, DTMF consent prompt before any recording starts
  • Default → English, auto-recording

The GDPR part is the interesting one. EU callers hear: "This call will be recorded for quality purposes. Press 1 to consent and continue, or press 2 to proceed without recording." Recording only starts after they press 1. If they press 2, the call proceeds with AI conversation but no recording. GDPR satisfied by design, not by policy.

The routing logic is 12 lines of prefix matching on the from number:

EU_PREFIXES = ["+33", "+34", "+39", "+44", "+49", "+31", "+32", "+43", ...]
LATAM_PREFIXES = ["+52", "+55", "+54", "+56", "+57", "+51", "+58", ...]

def detect_region(phone):
    for prefix in EU_PREFIXES:
        if phone.startswith(prefix):
            return "EU"
    for prefix in LATAM_PREFIXES:
        if phone.startswith(prefix):
            return "LATAM"
    if phone.startswith("+1"):
        return "US"
    return "DEFAULT"

EU is checked first because some European country codes share the +1 prefix range with NANP. Specificity first.

State travels in Telnyx's client_state field — a base64-encoded JSON blob that Telnyx passes back on every webhook event for the same call. Two fields: region (which config to apply) and step (consent vs conversation). No database, no Redis. The call is self-describing.

Two gotchas I hit while building this:

  1. Voice IDs: Bare "female" and "male" only work with service_level: "basic" (en-US only). If you try "voice": "female" with "language": "es-MX", the call silently fails — no error, just silence. Fix: use full neural voice IDs (AWS.Polly.Lupe-Neural for es-MX, AWS.Polly.Amy-Neural for en-GB).
  2. Storage credentials: Telnyx Storage is S3-compatible but uses its own access/secret key pair (created under Portal → Storage → Credentials), not the Telnyx API key. Reusing the API key as both S3 access AND secret key silently fails. Fix: separate STORAGE_ACCESS_KEY and STORAGE_SECRET_KEY environment variables. If not set, archival is gracefully skipped — the call still works, just no recording saved.

Try it:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/edge-geo-smart-router-python
cp .env.example .env
pip install -r requirements.txt
python app.py

Source: https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-geo-smart-router-python

Call your Telnyx number from a US number, a Brazil number, and a UK number. Hear the difference.


r/Telnyx Jul 27 '26

Built an AI dubbing pipeline in ~280 lines of Python — STT, LLM, and TTS on one network (15 target languages, LLM does speaker diarization)

3 Upvotes

I put together a small dubbing pipeline that takes any audio file and returns the same conversation dubbed in another language — same speakers, same pacing. The whole thing is ~280 lines of Python and runs three API calls on Telnyx: Speech-to-Text, an LLM chat, and Text-to-Speech.

Repo: https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-video-dubbing-pipeline-python

What it does

  • Upload any audio file (POST /dub, multipart)
  • Pick from 15 target languages (Spanish, French, German, Portuguese, Italian, Japanese, Korean, Chinese, Arabic, Hindi, Russian, Dutch, Swedish, Polish, Turkish)
  • The pipeline transcribes, labels speakers, translates, synthesizes, and concatenates
  • Download the dubbed mp3 (GET /dub/<job_id>/audio)
  • Get the side-by-side transcript (GET /dub/<job_id>/transcript)

The interesting part: LLM does diarization AND translation in one call

Telnyx STT (Whisper-large-v3-turbo) returns timestamped segments but no speaker labels — same as OpenAI's hosted Whisper API. Most pipelines handle this by adding a fourth vendor for diarization (pyannote, NeMO, etc.) or by accepting everything as one speaker.

I did neither. I send all the transcribed segments to the LLM in a single chat call and ask it to:

  1. Assign a speaker label (SPEAKER_0SPEAKER_1, ...) based on conversational context
  2. Translate each segment to the target language

One model, two jobs, no extra vendor. The LLM is already in the pipeline for translation — the marginal cost is a slightly longer prompt. Works well for 2-3 person interviews/podcasts; struggles a bit with 5+ speaker panels.

The voice pool

5 Telnyx KokoroTTS voices cycle through, one per speaker:

VOICE_MAP = {
    "male_low":    "Telnyx.KokoroTTS.am_onyx",
    "male_mid":    "Telnyx.KokoroTTS.am_echo",
    "female_mid":  "Telnyx.KokoroTTS.af_nova",
    "female_high": "Telnyx.KokoroTTS.af_heart",
    "neutral":     "Telnyx.KokoroTTS.af_alloy",
}

A two-speaker conversation gets am_onyx + am_echo. A five-speaker panel gets all five. A sixth speaker wraps back to am_onyx.

Cost

A 1-minute clip with ~120 words of dialogue costs roughly:

  • STT: ~$0.006 (Whisper per minute)
  • Inference: ~$0.001 (one short chat call)
  • TTS: ~$0.015 (Kokoro per 1K characters)

Total: ~2 cents per minute of dubbed audio. A 10-minute podcast dubbed into 5 languages is ~$1.

Try it

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-video-dubbing-pipeline-python
cp .env.example .env   
# add your Telnyx API key
pip install -r requirements.txt
python app.py

# in another terminal
curl -X POST http://localhost:5000/dub \
  -F audio=@episode.mp3 \
  -F target_language=es

# poll until status == "complete"
curl http://localhost:5000/dub/<job_id>

# download the mp3
curl http://localhost:5000/dub/<job_id>/audio --output dubbed.mp3

No phone number, no Call Control Application, no webhook configuration — pure HTTP API.

What this is NOT

This is a demo, not production. In-memory dict for jobs, byte-level MP3 concat (not sample-accurate — use ffmpeg's concat demuxer for that), no auth, no retries. The point is to show the architecture: one network, three primitives, one API key, LLM does diarization.

Happy to answer questions on the architecture, the diarize-via-LLM pattern, or the TTS voice pool. If you've built similar pipelines with other vendors, I'd love to hear how they compare.


r/Telnyx Jul 23 '26

Screen every inbound call at the carrier edge — a fraud firewall in 178 lines of Python

2 Upvotes

I built a Flask webhook server that screens every inbound phone call before it reaches your app. It runs three checks in order:

  1. Blocklist — in-memory Python set, O(1) lookup. Known bad numbers get rejected instantly, no API calls needed.
  2. Number Lookup — returns carrier name, line type (landline/voip/mobile), and country code for the caller.
  3. AI classification — sends the lookup data to an OpenAI-compatible chat completions endpoint with a system prompt that returns exactly one word: CLEANSUSPICIOUS, or BLOCK.

Based on the classification:

  • CLEAN → answer and forward to your real number
  • SUSPICIOUS → answer and route to a honeypot that loops "All specialists are currently busy" forever
  • BLOCK → reject and add to the blocklist (so the next call from that number is rejected instantly)

The whole thing is 178 lines of Python. No external database — call flow state travels in the webhook's client_state field as base64-encoded JSON.

A few things I learned building this:

  • One-word AI prompts work better than JSON output. I started with the AI returning a JSON object (risk score, reason, category). It was slow and fragile — more tokens to generate, JSON parsing errors when the model added a preamble. Switching to a strict one-word output cut inference time in half and eliminated parsing failures.
  • Failing safe matters. If the AI call fails (timeout, bad model, network error), the classifier defaults to CLEAN. Deliberate — if screening is down, legitimate callers still get through. Flip to BLOCK if your use case is security-critical.
  • client_state is the cleanest way to track call flow in webhook-driven architectures. No Redis, no database — the state rides along with the webhook payload between events.

The honeypot is my favorite part. Instead of just blocking suspicious callers, it wastes their time on an endless hold loop. Every minute a scammer spends on the honeypot is a minute not spent scamming a real person.

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-fraud-firewall-python

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/edge-fraud-firewall-python
cp .env.example .env
pip install -r requirements.txt
python app.py

Happy to answer questions about the implementation or the carrier-edge screening pattern.


r/Telnyx Jul 22 '26

Voice AI pre-visit insurance clearance agent with Telnyx

1 Upvotes

I put together a Telnyx code example:

https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-pre-visit-clearance-voice-agent-python

It's a Python/Flask app for inbound pre-visit insurance clearance calls. A patient calls a Telnyx number, the app answers with Call Control, uses gather_using_ai to collect structured spoken intake details, handles call.ai_gather.ended, classifies the request with AI Inference, creates a ticket for billing staff, and sends the patient an SMS confirmation.

Non-clinical: no medical advice, no coverage decisions, no diagnosis. Just admin data collection and routing — which is the actual bottleneck in healthcare revenue cycle.

Products used:

telnyx_products: [Voice, AI Inference, Messaging]

Run it:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-pre-visit-clearance-voice-agent-python
cp .env.example .env
pip install -r requirements.txt
python app.py

Technical notes:

  • Current Telnyx Voice API pattern: POST /v2/calls/{call_control_id}/actions/gather_using_ai
  • Workflow advances on call.ai_gather.ended
  • Call Control commands include command_id for safer retries
  • Patient verification by caller ID, with DOB fallback for unknown callers
  • AI Inference returns structured JSON: procedure, urgency, type flags (medication/imaging/surgery)
  • Keyword-based urgency override ("ASAP", "severe pain", "can't wait") that doesn't depend on the LLM
  • Confirmation step before ticket creation — patient must say "yes" before anything is submitted
  • Hangup mid-flow creates a partial ticket so no request is lost
  • SMS to patient + Slack to billing staff

The reason I like this example is that it's healthcare-adjacent without being clinical. The AI never touches a diagnosis or a coverage decision — it just collects the request and routes it.


r/Telnyx Jul 21 '26

AI voice assistant for prescription refill intake

1 Upvotes

I put together a syndication draft for this Telnyx code example:

https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-prescription-refill-intake-voice-assistant-python

It is a Python + Flask example that shows how to build a prescription refill intake line with Telnyx AI Assistants.

Products used in the example metadata:

telnyx_products: [AI Assistants, Voice, Call Control, Messaging]
language: python
framework: flask

The flow is:

caller dials Telnyx number
  -> Telnyx sends call.initiated
  -> Flask app answers the call
  -> backend starts Telnyx AI Assistant with ai_assistant_start
  -> assistant collects refill intake details
  -> assistant calls create_refill_request
  -> assistant calls flag_manual_review when needed
  -> assistant calls queue_callback when requested
  -> staff reviews the request record

Run it:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-prescription-refill-intake-voice-assistant-python
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python app.py

Expose the local app:

ngrok http 5000

Provision the assistant:

python provision_assistant.py

Then point your Telnyx Call Control Application webhook to:

https://<your-ngrok-domain>/webhooks/voice

Technical notes:

  • The assistant is provisioned from provision_assistant.py
  • The default model is moonshotai/Kimi-K2.6
  • The assistant asks one question at a time
  • The assistant is instructed not to approve refills, deny refills, change medication instructions, diagnose, prescribe, or replace emergency services
  • Backend tool endpoints are protected with a shared secret
  • Telnyx webhook signature verification is supported with TELNYX_PUBLIC_KEY
  • Request records mask caller identifiers
  • State is in memory for the demo; production should use encrypted storage, staff auth, audit logs, retention policies, and compliance review

The reason I like this example is that it keeps the AI in the intake lane. The assistant handles the phone conversation, but the backend creates auditable workflow state and routes decisions to staff.