<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sonam</title>
    <description>The latest articles on DEV Community by Sonam (@sonam_50a41a4ced7e6b4f3fa).</description>
    <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4004574%2Ff1f1b787-1d13-4716-b3f7-c8d2d134849c.png</url>
      <title>DEV Community: Sonam</title>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sonam_50a41a4ced7e6b4f3fa"/>
    <language>en</language>
    <item>
      <title>Build a Stateful AI Agent for Event Sponsorships</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Thu, 10 Sep 2026 21:46:01 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-stateful-ai-agent-for-event-sponsorships-9pi</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-stateful-ai-agent-for-event-sponsorships-9pi</guid>
      <description>&lt;p&gt;An event booth can generate hundreds of conversations in a day, but the useful context often disappears as soon as an attendee walks away.&lt;/p&gt;

&lt;p&gt;Someone asks a detailed product question. Another person wants a demo but leaves before speaking to sales. A third enters a giveaway, then comes back later with a buying timeline. By the end of the event, the team has badge scans and scattered notes, but not a continuous record of what each person actually needed.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/event-sponsorship-agent" rel="noopener noreferrer"&gt;&lt;code&gt;event-sponsorship-agent&lt;/code&gt;&lt;/a&gt; example turns that fragmented workflow into one stateful application on Telnyx Edge Compute.&lt;/p&gt;

&lt;p&gt;It serves a branded microsite, answers questions through browser chat and messaging, detects the attendee's language, collects qualification details, stores leads in SQLDB, routes high-intent prospects to sales by SMS, schedules follow-up, and produces an attribution report.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs inside the application?
&lt;/h2&gt;

&lt;p&gt;The project is a TypeScript Edge function built around a &lt;code&gt;SponsorAgent&lt;/code&gt; actor. Each attendee or browser session maps to its own actor instance, giving the conversation a durable home.&lt;/p&gt;

&lt;p&gt;The application combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent SDK actors&lt;/strong&gt; for per-attendee conversation state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telnyx AI Inference&lt;/strong&gt; for language detection, translation, product answers, and contextual replies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmable Messaging&lt;/strong&gt; for SMS and WhatsApp conversations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLDB&lt;/strong&gt; for lead capture and attribution data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV&lt;/strong&gt; for request-rate counters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled tasks&lt;/strong&gt; for post-event follow-up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt; for follow-up when that is the attendee's preferred channel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Compute&lt;/strong&gt; for the microsite, APIs, and webhook handlers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The voice webhook is also included as an integration point for a future Call Control conversation flow. In the current sample it validates and routes the event, then logs the handoff rather than implementing a complete live voice exchange.&lt;/p&gt;

&lt;h2&gt;
  
  
  One attendee, one durable agent
&lt;/h2&gt;

&lt;p&gt;Inbound SMS and WhatsApp messages are routed by phone number. Browser conversations are routed by session ID.&lt;/p&gt;

&lt;p&gt;Conceptually, the handler does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SPONSOR_AGENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attendeeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleChatMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting actor owns the attendee's channel, language, qualification step, collected details, giveaway status, demo request, and follow-up state.&lt;/p&gt;

&lt;p&gt;That matters because event conversations rarely happen in one perfectly ordered exchange. An attendee might ask a product question first, request a demo ten minutes later, and only then provide their company and timeline. The agent can continue the same workflow instead of rebuilding context from every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The qualification flow
&lt;/h2&gt;

&lt;p&gt;The agent guides an attendee through a short sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name&lt;/li&gt;
&lt;li&gt;Company&lt;/li&gt;
&lt;li&gt;Use case&lt;/li&gt;
&lt;li&gt;Company size&lt;/li&gt;
&lt;li&gt;Implementation timeline&lt;/li&gt;
&lt;li&gt;Preferred follow-up channel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each response updates the actor state and is upserted into SQLDB. The same &lt;code&gt;processMessage()&lt;/code&gt; method also recognizes giveaway, demo-booking, and product-question intents.&lt;/p&gt;

&lt;p&gt;For product questions, the actor calls Telnyx AI Inference through the native binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELNYX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCompletion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;moonshotai/Kimi-K2.6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample first detects the attendee's language and stores the ISO language code in session state. Later answers and qualification prompts can then be returned in that language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing a hot lead while they are still at the booth
&lt;/h2&gt;

&lt;p&gt;A CRM record is useful, but timing is the interesting part of an event workflow.&lt;/p&gt;

&lt;p&gt;When an attendee completes the qualification fields and requests a demo, the agent sends an SMS alert to the sales team immediately. The notification contains the captured context, including the company, use case, size, and timeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELNYX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;salesTeamNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hotLeadSummary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives a sales representative the opportunity to start a relevant conversation while the attendee is still nearby, rather than discovering the lead in a report the following week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling follow-up without a separate queue
&lt;/h2&gt;

&lt;p&gt;After qualification, the attendee can choose SMS, WhatsApp, email, or a call as the preferred follow-up channel. The actor schedules the work with the Agent SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delaySeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sendFollowUp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the task runs, the agent retrieves the lead from SQLDB, falls back to its durable state if necessary, asks the inference model to create a concise message, and sends or logs the result for the selected channel.&lt;/p&gt;

&lt;p&gt;Demo deployments use a short delay so the workflow is easy to observe. A live deployment can schedule the follow-up for the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring more than badge scans
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;GET /api/report&lt;/code&gt; endpoint queries SQLDB and returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total captured leads&lt;/li&gt;
&lt;li&gt;Total qualified leads&lt;/li&gt;
&lt;li&gt;Total converted or giveaway-entry leads&lt;/li&gt;
&lt;li&gt;Lead counts by interaction channel&lt;/li&gt;
&lt;li&gt;Lead counts by use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is intentionally a compact report, but the data model can be extended with campaign IDs, event locations, session attendance, meetings completed, or downstream CRM outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API surface
&lt;/h2&gt;

&lt;p&gt;The main routes are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET  /                  Branded event microsite
POST /api/chat          Browser chat
POST /api/followup      Schedule attendee follow-up
GET  /api/report        Lead attribution report
POST /webhook/sms       Inbound SMS
POST /webhook/whatsapp  Inbound WhatsApp
POST /webhook/voice     Voice integration scaffold
GET  /health            Health check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Messaging webhooks are verified with the Telnyx Ed25519 signature before their payloads are accepted. The interaction routes also use KV-backed limits: ten messaging requests, five voice events, or twenty browser-chat requests per identifier in a 60-second window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it locally
&lt;/h2&gt;

&lt;p&gt;Clone the examples repository and enter the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/event-sponsorship-agent
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project includes a local development adapter with in-memory KV, SQLite, and stubbed communications calls. This lets you open the microsite and exercise the chat, lead, rate-limit, and reporting flows without sending real messages.&lt;/p&gt;

&lt;p&gt;For an Edge deployment, configure the actor, SQLDB, KV, Telnyx binding, and required secrets in &lt;code&gt;telnyx.toml&lt;/code&gt;, then ship it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge types
telnyx-edge ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;DEMO_MODE=true&lt;/code&gt; while testing. Before enabling real communications, replace every placeholder, configure approved sending resources, verify webhook signatures, and review the applicable messaging requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this pattern goes next
&lt;/h2&gt;

&lt;p&gt;This example is framed around event sponsorship, but the architecture applies anywhere one real-world relationship needs persistent context across communications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conference attendee concierge&lt;/li&gt;
&lt;li&gt;Trade-show lead qualification&lt;/li&gt;
&lt;li&gt;Field marketing follow-up&lt;/li&gt;
&lt;li&gt;Partner onboarding&lt;/li&gt;
&lt;li&gt;Customer success events&lt;/li&gt;
&lt;li&gt;Product-launch registration and support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful pattern is not simply "add a chatbot to a microsite." It is giving each attendee a durable agent that can remember the conversation, act across channels, and connect the interaction to measurable business outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/event-sponsorship-agent" rel="noopener noreferrer"&gt;Event Sponsorship Agent source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/edge-compute" rel="noopener noreferrer"&gt;Telnyx Edge Compute documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/messaging" rel="noopener noreferrer"&gt;Telnyx messaging documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/inference" rel="noopener noreferrer"&gt;Telnyx AI Inference documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;Telnyx AI skills and toolkits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portal.telnyx.com/#/login/sign-up" rel="noopener noreferrer"&gt;Create a Telnyx account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would you want an event agent to do next: sync with a CRM, coordinate meeting availability, or score leads using post-event behavior?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>serverless</category>
      <category>telnyx</category>
    </item>
    <item>
      <title>Build an SMS Two-Factor Authentication Agent at the Edge</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:17:12 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-sms-two-factor-authentication-agent-at-the-edge-4eji</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-sms-two-factor-authentication-agent-at-the-edge-4eji</guid>
      <description>&lt;p&gt;Sending an SMS verification code takes one API call. Managing everything around that call is the real engineering problem.&lt;/p&gt;

&lt;p&gt;A two-factor authentication flow needs to generate a code, limit repeated requests, store the code safely, reject bad attempts, expire it on time, and make it unusable after successful verification. A traditional implementation often adds a web server, database, cache, cleanup worker, and secrets-management layer before the first message is sent.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/sms-two-factor-agent" rel="noopener noreferrer"&gt;&lt;code&gt;sms-two-factor-agent&lt;/code&gt;&lt;/a&gt; to explore a smaller architecture. It runs as a TypeScript agent on Telnyx Edge Compute and uses four primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One durable actor per phone number&lt;/li&gt;
&lt;li&gt;Telnyx KV with a five-minute TTL&lt;/li&gt;
&lt;li&gt;Scheduled cleanup through the Agent SDK&lt;/li&gt;
&lt;li&gt;SMS delivery through the native Telnyx binding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a complete verification-code lifecycle behind two HTTP endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API surface
&lt;/h2&gt;

&lt;p&gt;The application exposes two main routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /verify   Generate and send a six-digit code
POST /check    Validate a submitted code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A client starts verification with a placeholder E.164 number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://&amp;lt;your-function&amp;gt;.telnyxcompute.com/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"phone":"+1555XXXXXXX"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After receiving the code, the client submits it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://&amp;lt;your-function&amp;gt;.telnyxcompute.com/check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"phone":"+1555XXXXXXX","code":"123456"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Successful verification deletes the stored code immediately, making it single-use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use one actor per phone number?
&lt;/h2&gt;

&lt;p&gt;The request handler derives an actor identity from the phone number and routes both operations to that same actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AGENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;actorNameFromPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AGENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each actor owns the verification state for one phone number, including request attempts, failed checks, and the current rate-limit window. Calls for that actor are serialized, which makes per-number state easier to reason about when requests arrive at nearly the same time.&lt;/p&gt;

&lt;p&gt;Different phone numbers map to different actors, so their state remains isolated and they can be processed independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expiring codes without a cleanup worker
&lt;/h2&gt;

&lt;p&gt;The primary code store is Telnyx KV. Each generated code is written with a 300-second TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key expires automatically after five minutes, so there is no table to sweep and no periodic cleanup job to maintain.&lt;/p&gt;

&lt;p&gt;The agent also schedules an expiry task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expireCode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That task provides a deterministic place to remove any remaining code data and reset the actor's counters. The current sample also falls back to durable actor storage if a KV write fails, recording an explicit expiration timestamp so the fallback copy cannot remain valid indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending SMS through the Telnyx binding
&lt;/h2&gt;

&lt;p&gt;In live mode, the actor sends the verification message through the Edge runtime's Telnyx binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELNYX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fromNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Your verification code is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. It expires in 5 minutes.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binding gives the deployed function a pre-authenticated Telnyx client. The application does not need to hardcode an API key or construct an authorization header for every message.&lt;/p&gt;

&lt;p&gt;The sample starts in &lt;code&gt;DEMO_MODE&lt;/code&gt;, where generated codes are written to the actor logs instead of being sent. That lets you test routing, expiry, verification, and rate limiting before connecting a real SMS-capable number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting belongs with the verification state
&lt;/h2&gt;

&lt;p&gt;The actor allows five send attempts in a five-minute window. Because the counter is stored with the phone number's durable state, the limit survives ordinary process restarts and does not require a separate rate-limiting service.&lt;/p&gt;

&lt;p&gt;Failed verification attempts are tracked separately. A correct code clears the code and resets the counters; an incorrect code decrements the remaining attempts returned to the client.&lt;/p&gt;

&lt;p&gt;This is a useful property of the actor model: the behavior and the state it protects live together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the example
&lt;/h2&gt;

&lt;p&gt;Clone the code and install its dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/sms-two-factor-agent
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run typecheck
npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the actor function and KV namespace with the Telnyx Edge CLI, place the returned identifiers in &lt;code&gt;telnyx.toml&lt;/code&gt;, and deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge new-func &lt;span class="nt"&gt;--actor&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; ts &lt;span class="nt"&gt;-n&lt;/span&gt; sms-two-factor-agent
telnyx-edge storage kv create &lt;span class="nt"&gt;--name&lt;/span&gt; sms-two-factor-agent-2fa
telnyx-edge ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep demo mode enabled for the first run. When you are ready to send real messages, configure an SMS-capable Telnyx number, set &lt;code&gt;DEMO_MODE&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;, and deploy again. If you send application-to-person traffic to US numbers, make sure the sender and use case satisfy the applicable 10DLC requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to take it next
&lt;/h2&gt;

&lt;p&gt;This sample deliberately focuses on the verification lifecycle rather than a complete identity system. Before using the pattern in production, consider adding hashed codes, stronger abuse controls, audit events, delivery-status handling, idempotency, tenant isolation, and a recovery path.&lt;/p&gt;

&lt;p&gt;The broader pattern also applies beyond 2FA. Per-recipient actors, expiring state, scheduled work, and native communications bindings are useful for passwordless login, account recovery, consent confirmation, time-sensitive alerts, and other workflows where state and messaging must move together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/sms-two-factor-agent" rel="noopener noreferrer"&gt;SMS Two-Factor Agent source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/edge-compute" rel="noopener noreferrer"&gt;Telnyx Edge Compute documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/messaging" rel="noopener noreferrer"&gt;Telnyx messaging documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;Telnyx AI skills and toolkits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portal.telnyx.com/#/login/sign-up" rel="noopener noreferrer"&gt;Create a Telnyx account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How would you extend this pattern: stricter risk scoring, multiple delivery channels, or a full passwordless sign-in flow?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>security</category>
      <category>serverless</category>
      <category>telnyx</category>
    </item>
    <item>
      <title>Build an AI Event Microsite on Telnyx Edge Compute</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Fri, 04 Sep 2026 21:18:42 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-event-microsite-on-telnyx-edge-compute-1ek</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-event-microsite-on-telnyx-edge-compute-1ek</guid>
      <description>&lt;p&gt;Most event apps quietly become several apps.&lt;/p&gt;

&lt;p&gt;You start with a landing page. Then someone asks for SMS updates. Then attendees want a way to ask questions. Sponsors want lead capture. Organizers want feedback. Someone wants a daily report.&lt;/p&gt;

&lt;p&gt;Suddenly your "event site" is a static frontend, a webhook server, a chatbot, a voice app, a database, an email job, and a few scripts holding it together.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;edge-event-microsite&lt;/code&gt; example compresses that into one deploy.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-event-microsite" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-event-microsite&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it builds
&lt;/h2&gt;

&lt;p&gt;This is a TypeScript app running on Telnyx Edge Compute.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a server-rendered event microsite&lt;/li&gt;
&lt;li&gt;event schedule and sponsor data stored in Telnyx KV&lt;/li&gt;
&lt;li&gt;an SMS/WhatsApp AI concierge&lt;/li&gt;
&lt;li&gt;in-browser voice AI with WebRTC&lt;/li&gt;
&lt;li&gt;lead qualification with Telnyx AI Inference&lt;/li&gt;
&lt;li&gt;voice feedback transcription&lt;/li&gt;
&lt;li&gt;sponsor reporting&lt;/li&gt;
&lt;li&gt;webhook signature verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is that all of these surfaces read from the same source of truth: one Telnyx KV namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;At a high level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attendee browser
  -&amp;gt; Edge function
  -&amp;gt; Telnyx KV

SMS / WhatsApp message
  -&amp;gt; Edge function
  -&amp;gt; Telnyx AI Inference
  -&amp;gt; Telnyx Messaging
  -&amp;gt; Telnyx KV

Browser voice agent
  -&amp;gt; WebRTC
  -&amp;gt; AI assistant tool callback
  -&amp;gt; Edge function
  -&amp;gt; Telnyx KV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The site, the text concierge, and the voice assistant all use the same event data.&lt;/p&gt;

&lt;p&gt;Change the schedule once, and every surface can answer from the same updated state.&lt;/p&gt;

&lt;p&gt;No sync jobs. No duplicated config. No stale FAQ hiding in a separate bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Edge Compute fits
&lt;/h2&gt;

&lt;p&gt;An event experience has a lot of small, latency-sensitive interactions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;render the schedule&lt;/li&gt;
&lt;li&gt;answer "where is the keynote?"&lt;/li&gt;
&lt;li&gt;capture a lead&lt;/li&gt;
&lt;li&gt;route a hot lead to sales&lt;/li&gt;
&lt;li&gt;transcribe attendee feedback&lt;/li&gt;
&lt;li&gt;generate a sponsor report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those need a giant backend.&lt;/p&gt;

&lt;p&gt;They need a fast place to run close to the communications layer.&lt;/p&gt;

&lt;p&gt;That is the useful pattern in this sample: the Edge function becomes the backend for the website, the messaging workflows, and the voice assistant tool calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  KV as the shared event brain
&lt;/h2&gt;

&lt;p&gt;The sample uses Telnyx KV for event state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event details&lt;/li&gt;
&lt;li&gt;schedule items&lt;/li&gt;
&lt;li&gt;sponsor information&lt;/li&gt;
&lt;li&gt;attendees&lt;/li&gt;
&lt;li&gt;leads&lt;/li&gt;
&lt;li&gt;feedback&lt;/li&gt;
&lt;li&gt;assistant configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The microsite reads from KV when rendering pages and APIs.&lt;/p&gt;

&lt;p&gt;The SMS/WhatsApp concierge reads from KV when answering attendee questions.&lt;/p&gt;

&lt;p&gt;The voice assistant can call back into the same Edge function to look up event information.&lt;/p&gt;

&lt;p&gt;That shared state is what keeps the experience coherent.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI concierge for SMS and WhatsApp
&lt;/h2&gt;

&lt;p&gt;Attendees can ask questions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where is the keynote?
What time is the workshop?
Who is sponsoring the afterparty?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Edge function uses Telnyx AI Inference to generate a useful answer from the event context, then sends the response back with Telnyx Messaging.&lt;/p&gt;

&lt;p&gt;The same flow can also qualify leads.&lt;/p&gt;

&lt;p&gt;If a message suggests strong buying intent, the app can extract company, role, and interest level, store that lead in KV, and route the hot lead to a sales rep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Voice AI in the browser
&lt;/h2&gt;

&lt;p&gt;The sample also includes an in-browser voice path.&lt;/p&gt;

&lt;p&gt;Instead of forcing attendees to call a phone number, the microsite can connect them to a Telnyx AI Assistant through WebRTC using &lt;code&gt;@telnyx/ai-agent-lib&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The assistant is provisioned server-side and can use a webhook tool that queries the same KV data as the site and SMS concierge.&lt;/p&gt;

&lt;p&gt;That gives attendees three ways to interact with the same event backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browse the site&lt;/li&gt;
&lt;li&gt;text the concierge&lt;/li&gt;
&lt;li&gt;talk to the assistant&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feedback transcription and sponsor reports
&lt;/h2&gt;

&lt;p&gt;After a session, attendees can leave spoken feedback.&lt;/p&gt;

&lt;p&gt;The app transcribes the audio, summarizes it with AI, and stores the result.&lt;/p&gt;

&lt;p&gt;From there, the organizer can generate a sponsor report with lead counts, feedback summaries, and engagement metrics.&lt;/p&gt;

&lt;p&gt;This is where the example starts to feel less like a website and more like an event operating system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Clone the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/edge-event-microsite
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authenticate and create a KV namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge auth api-key &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;YOUR_TELNYX_API_KEY&amp;gt;
telnyx-edge storage kv create &lt;span class="nt"&gt;--name&lt;/span&gt; edge-event-microsite-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add webhook verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PUBLIC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TELNYX_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.telnyx.com/v2/public_key | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.public'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

telnyx-edge secrets add TELNYX_PUBLIC_KEY &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PUBLIC_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function deploys to a &lt;code&gt;*.telnyxcompute.com&lt;/code&gt; URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;Before using this for a real event, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication for organizer/admin routes&lt;/li&gt;
&lt;li&gt;stricter permissions around lead exports&lt;/li&gt;
&lt;li&gt;rate limits for public messaging and voice flows&lt;/li&gt;
&lt;li&gt;moderation for attendee-submitted feedback&lt;/li&gt;
&lt;li&gt;consent language for transcription&lt;/li&gt;
&lt;li&gt;retries for sponsor report delivery&lt;/li&gt;
&lt;li&gt;analytics around which channel attendees prefer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample is intentionally compact, but the architecture is the interesting part.&lt;/p&gt;

&lt;p&gt;One Edge function can serve the event website, answer attendee questions, power a voice assistant, capture feedback, and report back to sponsors.&lt;/p&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-event-microsite" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-event-microsite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Edge Compute docs: &lt;a href="https://developers.telnyx.com/docs/edge-compute" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/edge-compute&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference docs: &lt;a href="https://developers.telnyx.com/docs/inference" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/inference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Messaging docs: &lt;a href="https://developers.telnyx.com/docs/messaging" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/messaging&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI Assistants docs: &lt;a href="https://developers.telnyx.com/docs/inference/ai-assistants" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/inference/ai-assistants&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI skills and toolkits: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>edge</category>
      <category>typescript</category>
      <category>telnyx</category>
    </item>
    <item>
      <title>Build an AI Meeting Facilitator on Telnyx Edge Compute</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Fri, 04 Sep 2026 03:27:37 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-meeting-facilitator-on-telnyx-edge-compute-3l0n</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-meeting-facilitator-on-telnyx-edge-compute-3l0n</guid>
      <description>&lt;p&gt;Most meeting tools wait until the call is over.&lt;/p&gt;

&lt;p&gt;They record, transcribe, summarize, and then hand you notes after the damage is already done.&lt;/p&gt;

&lt;p&gt;That is useful, but it misses the live part of the meeting: who got interrupted, who has not spoken yet, and whether the group is actually making decisions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;conference-agent-mediator&lt;/code&gt; example takes a different approach. It puts an AI participant directly into a Telnyx conference bridge.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/conference-agent-mediator" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/conference-agent-mediator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it builds
&lt;/h2&gt;

&lt;p&gt;This is a TypeScript app running on Telnyx Edge Compute with the Telnyx Agent SDK.&lt;/p&gt;

&lt;p&gt;The agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;joins a Telnyx conference as a participant&lt;/li&gt;
&lt;li&gt;starts live transcription for each speaker&lt;/li&gt;
&lt;li&gt;stores transcript turns in durable actor state&lt;/li&gt;
&lt;li&gt;checks the conversation every 30 seconds&lt;/li&gt;
&lt;li&gt;asks an LLM whether the room needs a facilitation nudge&lt;/li&gt;
&lt;li&gt;speaks that nudge into the conference&lt;/li&gt;
&lt;li&gt;summarizes the meeting when it ends&lt;/li&gt;
&lt;li&gt;sends the summary by SMS&lt;/li&gt;
&lt;li&gt;streams live state to observers over WebSocket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a meeting facilitator that is not just a post-call summary bot. It is in the room while the conversation is happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;p&gt;At a high level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Caller joins conference
  -&amp;gt; ConferenceAgent actor is created
  -&amp;gt; participant audio is transcribed
  -&amp;gt; transcript turns are stored
  -&amp;gt; mediation loop checks who has spoken
  -&amp;gt; AI generates a short facilitator prompt
  -&amp;gt; Telnyx speaks the prompt into the bridge
  -&amp;gt; meeting ends
  -&amp;gt; AI summary is generated
  -&amp;gt; SMS summary is sent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main idea is one actor per conference.&lt;/p&gt;

&lt;p&gt;That actor owns the state for the meeting: participants, transcript, prompts, phase, and final summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why actors fit this problem
&lt;/h2&gt;

&lt;p&gt;A conference is stateful.&lt;/p&gt;

&lt;p&gt;Events arrive over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a participant joins&lt;/li&gt;
&lt;li&gt;transcription starts&lt;/li&gt;
&lt;li&gt;a speaker says something&lt;/li&gt;
&lt;li&gt;another speaker joins&lt;/li&gt;
&lt;li&gt;the mediator decides whether to intervene&lt;/li&gt;
&lt;li&gt;the conference ends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this were split across a stateless webhook handler, a worker, a database, and a scheduler, you would spend a lot of time rebuilding context.&lt;/p&gt;

&lt;p&gt;With the Agent SDK, the conference itself can become the unit of state.&lt;/p&gt;

&lt;p&gt;The sample keeps the transcript and participant state on the &lt;code&gt;ConferenceAgent&lt;/code&gt;, so every webhook and timer callback has the context it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mediation loop
&lt;/h2&gt;

&lt;p&gt;The app uses a crash-safe timer that runs every 30 seconds.&lt;/p&gt;

&lt;p&gt;Each pass checks whether the conference is live, looks at recent transcript turns, finds quiet participants, and asks Telnyx AI Inference for a short facilitator line.&lt;/p&gt;

&lt;p&gt;The prompt is intentionally conservative. The agent should not constantly interrupt. It should only step in when there is a useful reason, like bringing a quiet person into the conversation or helping the group return to a decision.&lt;/p&gt;

&lt;p&gt;Example mediator output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before we move on, I would like to hear from Dana, who has not had a chance to weigh in yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In live mode, the agent speaks that line into the conference bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  API and runtime pieces
&lt;/h2&gt;

&lt;p&gt;The example includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /webhooks/voice&lt;/code&gt; for Telnyx Call Control events&lt;/li&gt;
&lt;li&gt;an Agent SDK actor for conference state&lt;/li&gt;
&lt;li&gt;live transcription events from Telnyx&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference for mediation and summary generation&lt;/li&gt;
&lt;li&gt;Telnyx Messaging for the post-call SMS summary&lt;/li&gt;
&lt;li&gt;an observer socket for live transcript and state updates&lt;/li&gt;
&lt;li&gt;demo mode so you can test the pipeline without real phone calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is that these are not stitched together across several vendors.&lt;/p&gt;

&lt;p&gt;Voice, transcription, LLM inference, stateful agents, and SMS all sit behind the same Telnyx platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the example
&lt;/h2&gt;

&lt;p&gt;Clone the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/conference-agent-mediator
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set your runtime values with placeholders first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge secrets add TELNYX_API_KEY &amp;lt;YOUR_TELNYX_API_KEY&amp;gt;
telnyx-edge secrets add SMS_FROM &amp;lt;YOUR_TELNYX_SMS_NUMBER&amp;gt;
telnyx-edge secrets add SMS_TO &amp;lt;SUMMARY_RECIPIENT_NUMBER&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your Telnyx Call Control application webhook at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://conference-agent-mediator-&amp;lt;id&amp;gt;.telnyxcompute.com/webhooks/voice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample also supports demo mode, which is useful when you want to exercise the actor, transcript, mediation, and summary flow before wiring up live calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like about this pattern
&lt;/h2&gt;

&lt;p&gt;The interesting part is not only that AI can summarize a meeting.&lt;/p&gt;

&lt;p&gt;The interesting part is that the agent can participate while the meeting is still happening.&lt;/p&gt;

&lt;p&gt;Because the agent runs close to the communications path, it can react to the live transcript quickly enough for a mediation prompt to still feel relevant.&lt;/p&gt;

&lt;p&gt;That opens up a broader pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;meeting facilitators&lt;/li&gt;
&lt;li&gt;sales-call coaches&lt;/li&gt;
&lt;li&gt;support-call supervisors&lt;/li&gt;
&lt;li&gt;interview note takers&lt;/li&gt;
&lt;li&gt;training assistants&lt;/li&gt;
&lt;li&gt;live compliance monitors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any workflow where the assistant needs to observe a conversation, keep state, and act during the call can use the same foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;Before using this with real meetings, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication for observer sockets&lt;/li&gt;
&lt;li&gt;participant consent flows&lt;/li&gt;
&lt;li&gt;retention controls for transcripts&lt;/li&gt;
&lt;li&gt;redaction for sensitive data&lt;/li&gt;
&lt;li&gt;stronger speaker identity mapping&lt;/li&gt;
&lt;li&gt;prompt tuning for different meeting cultures&lt;/li&gt;
&lt;li&gt;monitoring for transcription, inference, and SMS delivery&lt;/li&gt;
&lt;li&gt;explicit controls for when the agent is allowed to speak&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample keeps the moving parts small on purpose, but the architecture is the useful thing: one durable agent owns one live conversation.&lt;/p&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/conference-agent-mediator" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/conference-agent-mediator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Agent SDK docs: &lt;a href="https://developers.telnyx.com/docs/agent-sdk" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/agent-sdk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Edge Compute docs: &lt;a href="https://developers.telnyx.com/docs/edge-compute" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/edge-compute&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Call Control API reference: &lt;a href="https://developers.telnyx.com/api-reference/call-control" rel="noopener noreferrer"&gt;https://developers.telnyx.com/api-reference/call-control&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference docs: &lt;a href="https://developers.telnyx.com/docs/inference" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/inference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Messaging docs: &lt;a href="https://developers.telnyx.com/docs/messaging" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/messaging&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI skills and toolkits: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>voice</category>
      <category>edge</category>
      <category>telnyx</category>
    </item>
    <item>
      <title>Build an Agent That Turns Voicemails Into SMS Summaries</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:30:46 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-agent-that-turns-voicemails-into-sms-summaries-4hp4</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-agent-that-turns-voicemails-into-sms-summaries-4hp4</guid>
      <description>&lt;p&gt;Voicemail is one of those workflows that still feels weirdly stuck.&lt;/p&gt;

&lt;p&gt;Someone leaves a message. You get a badge. Then you have to listen through a minute of audio just to find the ten seconds that matter: who called, what they need, whether there is a deadline, and whether you need to call back.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;voicemail-to-sms-agent&lt;/code&gt; example turns that into an event-driven AI workflow.&lt;/p&gt;

&lt;p&gt;When a voicemail is left, the agent transcribes the recording, summarizes it with an LLM, sends the summary over SMS, and archives the original audio. The result is closer to voicemail triage than voicemail playback.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/voicemail-to-sms-agent" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/voicemail-to-sms-agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the app does
&lt;/h2&gt;

&lt;p&gt;This is a TypeScript example built for Telnyx Edge.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Caller leaves voicemail
  -&amp;gt; Call Control webhook fires
  -&amp;gt; Edge agent receives the task
  -&amp;gt; agent downloads the recording
  -&amp;gt; Speech-to-Text transcribes the audio
  -&amp;gt; AI Inference summarizes the transcript
  -&amp;gt; Messaging sends the SMS summary
  -&amp;gt; Cloud Storage archives the audio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of checking voicemail manually, the mailbox owner gets a concise text message with the important parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trigger
&lt;/h2&gt;

&lt;p&gt;The agent reacts to a Call Control webhook.&lt;/p&gt;

&lt;p&gt;In the sample, the agent checks that the incoming task is a voicemail event before doing any work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;WebhookPayload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;call.status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;call_status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voicemail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ignored&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps the agent focused. It does not process every call event. It only starts the pipeline when the call actually ends in voicemail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Once the voicemail event is accepted, the agent pulls the key pieces out of the webhook payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callControlId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;call_control_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recordingId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recording&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callerNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it downloads the audio recording through the Telnyx binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downloadRecording&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;call_control_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callControlId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;recording_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recordingId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the recording becomes text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;audioBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That transcript is what the LLM sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarizing the voicemail
&lt;/h2&gt;

&lt;p&gt;The summary step uses Telnyx AI through an OpenAI-compatible chat completion call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summaryResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCompletion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarize the following voicemail transcription into a concise SMS message.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Caller: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callerNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nTranscription: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transcriptText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM is not being asked to run the whole workflow. It has one job: take a transcript and make it useful enough to read quickly on a phone.&lt;/p&gt;

&lt;p&gt;That is the part I like about this pattern. The workflow stays explicit, but the language-heavy step gets handled by AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo mode vs live mode
&lt;/h2&gt;

&lt;p&gt;The sample includes a &lt;code&gt;LIVE_MODE&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;LIVE_MODE=false&lt;/code&gt;, the agent logs the SMS it would send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Demo mode: SMS not sent. Would send:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;destinationNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Voicemail from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callerNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;summaryText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;LIVE_MODE=true&lt;/code&gt;, it sends the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;TELNYX_SMS_NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;destinationNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Voicemail from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callerNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;summaryText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the example easier to test safely. You can run the whole pipeline first, inspect the generated SMS payload, and only flip to live mode once you are ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Archiving the original audio
&lt;/h2&gt;

&lt;p&gt;The agent also stores the voicemail recording in Telnyx Cloud Storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_BUCKET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voicemail-archives&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`voicemails/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;recordingId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.mp3`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;audioBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio/mpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a lightweight audit trail. The SMS summary is the fast path, but the original audio is still available if the owner needs to review it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is useful
&lt;/h2&gt;

&lt;p&gt;This example is small, but the pattern is production-shaped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;react to a communications event&lt;/li&gt;
&lt;li&gt;pull the relevant media&lt;/li&gt;
&lt;li&gt;run AI on it&lt;/li&gt;
&lt;li&gt;notify someone through the right channel&lt;/li&gt;
&lt;li&gt;store the source artifact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could adapt the same shape for customer support inboxes, missed sales calls, appointment reminders, after-hours service lines, or any workflow where voice messages need to become structured follow-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Clone the examples repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/voicemail-to-sms-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure values like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELNYX_API_KEY=&amp;lt;your_telnyx_api_key&amp;gt;
TELNYX_SMS_NUMBER=&amp;lt;your_telnyx_sms_number&amp;gt;
MAILBOX_OWNER_NUMBER=&amp;lt;summary_recipient_number&amp;gt;
STORAGE_BUCKET=voicemail-archives
LIVE_MODE=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then typecheck, test, and deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deployment, point your Call Control voicemail webhook at the deployed Edge URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code example: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/voicemail-to-sms-agent" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/voicemail-to-sms-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI toolkit: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Call Control docs: &lt;a href="https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference docs: &lt;a href="https://developers.telnyx.com/docs/inference" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/inference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Messaging docs: &lt;a href="https://developers.telnyx.com/docs/messaging" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/messaging&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Portal: &lt;a href="https://portal.telnyx.com" rel="noopener noreferrer"&gt;https://portal.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>telnyx</category>
      <category>typescript</category>
      <category>voice</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built an AI Call Router That Actually Understands Intent</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Mon, 31 Aug 2026 23:13:54 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/i-built-an-ai-call-router-that-actually-understands-intent-4aa</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/i-built-an-ai-call-router-that-actually-understands-intent-4aa</guid>
      <description>&lt;p&gt;I’ve always hated phone menus. "Press 1 for sales, 2 for support." By the time you reach the right department, you've listened to three minutes of hold music. I wanted to build something better: a call router that just asks you what you need and routes you instantly.&lt;/p&gt;

&lt;p&gt;I built this using Telnyx. Not just their voice API, but their new Edge Runtime. Here’s why that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Voice AI
&lt;/h2&gt;

&lt;p&gt;Building voice AI is usually a glue-code nightmare. You need a telephony provider, a speech-to-text service, an LLM provider, and a database to store your routing logic. That’s four different API keys, four different SDKs, and four points of failure.&lt;/p&gt;

&lt;p&gt;When latency spikes on the LLM, the caller waits. When the database connection drops, the call fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Telnyx Edge Runtime
&lt;/h2&gt;

&lt;p&gt;Telnyx is AI Communications Infrastructure. They own the telephony network, the AI inference layer, and now, an edge compute runtime.&lt;/p&gt;

&lt;p&gt;For this project, I used a &lt;code&gt;StatefulActor&lt;/code&gt; on the Edge Runtime. When a call comes in, the runtime spins up an actor dedicated to that call leg. This actor holds the call state in memory. No external session database is required.&lt;/p&gt;

&lt;p&gt;To classify the caller's intent, I used the built-in AI Inference binding. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELNYX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCompletion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta-llama/Llama-3.3-70B-Instruct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice anything missing? API keys. The binding is pre-authenticated. The runtime handles the credentials. I just call the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing with Telnyx KV
&lt;/h2&gt;

&lt;p&gt;Once the LLM tells me the caller wants "billing," I need to know where to transfer them. I stored the route table in Telnyx KV, a globally distributed key-value store.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;route:billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the key is missing, it falls back to a default destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Caller dials in.&lt;/li&gt;
&lt;li&gt;The actor answers and speaks a greeting.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gather_using_ai&lt;/code&gt; captures the caller's request.&lt;/li&gt;
&lt;li&gt;The AI Inference binding classifies the intent.&lt;/li&gt;
&lt;li&gt;KV looks up the destination.&lt;/li&gt;
&lt;li&gt;The call is transferred.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this happens in one edge function. No external servers. No glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;When you build on AI Communications Infrastructure, you stop stitching together disparate services. You start composing capabilities. The telephony, the AI, and the compute live in the same place. The latency is lower. The code is cleaner.&lt;/p&gt;

&lt;p&gt;If you want to see the code, I’ve open-sourced it. You can deploy it right now with the Telnyx CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-powered-call-router" rel="noopener noreferrer"&gt;Check out the code here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>telnyx</category>
      <category>ai</category>
      <category>voice</category>
      <category>edge</category>
    </item>
    <item>
      <title>Build an AI Call Router That Sends Callers to the Right Team</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Mon, 31 Aug 2026 21:43:03 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-call-router-that-sends-callers-to-the-right-team-1cpj</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-call-router-that-sends-callers-to-the-right-team-1cpj</guid>
      <description>&lt;p&gt;Traditional IVRs make callers do the routing work.&lt;/p&gt;

&lt;p&gt;You call a business, listen to a menu, remember the options, press a number, and hope you picked the right path. That works, but it is not how people naturally ask for help.&lt;/p&gt;

&lt;p&gt;Most callers already know what they want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I need to pay my bill."&lt;/li&gt;
&lt;li&gt;"I want to upgrade my plan."&lt;/li&gt;
&lt;li&gt;"I need help with support."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;ai-powered-call-router&lt;/code&gt; example turns that sentence into the routing decision.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-powered-call-router" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-powered-call-router&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the app builds
&lt;/h2&gt;

&lt;p&gt;This is a Python Flask app that handles inbound calls with Telnyx Call Control and classifies caller intent with Telnyx AI Inference.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbound call
  -&amp;gt; Telnyx sends a Call Control webhook
  -&amp;gt; Flask verifies the webhook
  -&amp;gt; app answers the call
  -&amp;gt; app speaks a greeting
  -&amp;gt; app gathers caller speech
  -&amp;gt; AI classifies intent
  -&amp;gt; app announces the transfer
  -&amp;gt; Call Control transfers the caller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of asking the caller to press buttons, the app asks what they need and routes based on their answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core pieces
&lt;/h2&gt;

&lt;p&gt;The example uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telnyx Call Control for answering, speaking, gathering, and transferring calls&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference for intent classification&lt;/li&gt;
&lt;li&gt;Telnyx webhook verification for signed inbound events&lt;/li&gt;
&lt;li&gt;a simple route table for mapping intents to destinations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source keeps the route table intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ROUTE_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1XXXXXXXXXX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1XXXXXXXXXX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1XXXXXXXXXX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real app, those destinations might be queues, agents, contact center flows, or PBX extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gathering caller speech
&lt;/h2&gt;

&lt;p&gt;Once the inbound call is answered, the app plays a greeting. After the greeting finishes, it starts speech capture using Call Control's AI gather action.&lt;/p&gt;

&lt;p&gt;The important detail is that the app waits for the &lt;code&gt;call.speak.ended&lt;/code&gt; event before starting the gather. That avoids the greeting and the caller's first response overlapping.&lt;/p&gt;

&lt;p&gt;The gather step produces a &lt;code&gt;call.ai_gather.ended&lt;/code&gt; event with the caller's utterance.&lt;/p&gt;

&lt;p&gt;That text becomes the input to the intent classifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classifying intent with AI
&lt;/h2&gt;

&lt;p&gt;The app sends the caller's transcribed request to Telnyx AI Inference and asks the model to classify it into one of the supported routes.&lt;/p&gt;

&lt;p&gt;The default model in the sample is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meta-llama/Llama-3.3-70B-Instruct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model does not need to write a long answer. It only needs to return a routing label such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;billing
sales
support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps the AI part narrow and practical. The LLM is not running the whole call center. It is doing one job: mapping natural language to a route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transferring the call
&lt;/h2&gt;

&lt;p&gt;After intent classification, the app speaks a short announcement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transferring you to billing. Please hold.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it waits for that announcement to finish before calling the transfer action.&lt;/p&gt;

&lt;p&gt;That makes the caller experience cleaner. They hear what is happening before the bridge is created.&lt;/p&gt;

&lt;p&gt;The example uses a blind transfer: Telnyx dials the destination and connects the original caller once the destination answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook verification
&lt;/h2&gt;

&lt;p&gt;Voice automation should not trust random HTTP requests.&lt;/p&gt;

&lt;p&gt;The app verifies inbound Telnyx webhooks with Ed25519 signature verification before processing the event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unwrap_with_ed25519&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TELNYX_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means call actions like answer, gather, and transfer only run after the app verifies the webhook came from Telnyx.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it locally
&lt;/h2&gt;

&lt;p&gt;Clone the examples repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/ai-powered-call-router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the required values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELNYX_API_KEY=&amp;lt;your_telnyx_api_key&amp;gt;
TELNYX_PUBLIC_KEY=&amp;lt;your_telnyx_public_key&amp;gt;
TELNYX_CONNECTION_ID=&amp;lt;your_call_control_application_id&amp;gt;
AI_MODEL=meta-llama/Llama-3.3-70B-Instruct
PORT=5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expose your local server with a tunnel and point your Telnyx Call Control Application webhook to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;your-tunnel-domain&amp;gt;/webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I like this pattern
&lt;/h2&gt;

&lt;p&gt;This is a good example of where AI is useful without making the system vague.&lt;/p&gt;

&lt;p&gt;The caller still moves through a deterministic telephony flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer the call&lt;/li&gt;
&lt;li&gt;gather speech&lt;/li&gt;
&lt;li&gt;classify intent&lt;/li&gt;
&lt;li&gt;look up a destination&lt;/li&gt;
&lt;li&gt;transfer the call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model only handles the messy human-language part.&lt;/p&gt;

&lt;p&gt;That is the sweet spot for a lot of production AI apps: use the LLM where language is ambiguous, but keep the workflow itself explicit and observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;Before using this with real callers, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries and fallback routing&lt;/li&gt;
&lt;li&gt;latency tracking for gather and classification&lt;/li&gt;
&lt;li&gt;route-level analytics&lt;/li&gt;
&lt;li&gt;auth around any admin/config endpoints&lt;/li&gt;
&lt;li&gt;call recording/transcript retention policy&lt;/li&gt;
&lt;li&gt;escalation rules for uncertain classifications&lt;/li&gt;
&lt;li&gt;monitoring for failed transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the core idea is simple: callers should be able to say what they need, and your app should route them without forcing them through a rigid menu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code example: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-powered-call-router" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-powered-call-router&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI toolkit: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Call Control docs: &lt;a href="https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference docs: &lt;a href="https://developers.telnyx.com/docs/inference" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/inference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Webhook signing: &lt;a href="https://developers.telnyx.com/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing" rel="noopener noreferrer"&gt;https://developers.telnyx.com/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Portal: &lt;a href="https://portal.telnyx.com" rel="noopener noreferrer"&gt;https://portal.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>telnyx</category>
      <category>python</category>
      <category>voice</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build a Natural Language IVR with Telnyx Call Control and AI Inference</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Fri, 28 Aug 2026 21:36:08 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-natural-language-ivr-with-telnyx-call-control-and-ai-inference-89f</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-natural-language-ivr-with-telnyx-call-control-and-ai-inference-89f</guid>
      <description>&lt;p&gt;Nobody likes phone trees. "Press 1 for billing, press 2 for support." Miss an option? Start over. It is friction at its worst.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;voice-ivr-with-agent-backend&lt;/code&gt; example replaces that with a natural language conversation. Callers just say what they need, and the app routes them to the right department.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it builds
&lt;/h2&gt;

&lt;p&gt;A Python/Flask app that handles inbound calls with a conversational IVR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbound Call
    -&amp;gt; answer with Call Control
    -&amp;gt; look up menu config from KV
    -&amp;gt; LLM generates a dynamic greeting
    -&amp;gt; gather(speech) — caller says what they need
    -&amp;gt; LLM routes intent to a department
    -&amp;gt; transfer call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The core primitives
&lt;/h2&gt;

&lt;p&gt;The app combines four Telnyx primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Call Control&lt;/strong&gt;: &lt;code&gt;answer()&lt;/code&gt;, &lt;code&gt;speak()&lt;/code&gt;, &lt;code&gt;gather_using_speech()&lt;/code&gt;, &lt;code&gt;transfer()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Inference&lt;/strong&gt;: &lt;code&gt;telnyx.ai.openai.chat.completions.create()&lt;/code&gt; for greetings and intent routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV store&lt;/strong&gt;: menu config per phone number (business name, departments, transfer numbers, keywords)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent state machine&lt;/strong&gt;: an &lt;code&gt;IVRAgent&lt;/code&gt; class that tracks call state, turn count, and retry logic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dynamic greeting via LLM
&lt;/h2&gt;

&lt;p&gt;Instead of a hardcoded "Press 1 for billing," the app generates a conversational greeting from the KV config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_dynamic_menu_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menu_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;departments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;menu_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;departments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;dept_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an IVR assistant for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;menu_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;business_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Available departments:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dept_list&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Greet the caller briefly and ask how you can help. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keep it conversational and under 2 sentences.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM generates the greeting through the OpenAI-compatible Telnyx Inference binding. If it fails, the app falls back to a static greeting from the KV config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent routing via LLM
&lt;/h2&gt;

&lt;p&gt;When the caller speaks, the transcription is passed to &lt;code&gt;route_intent_with_llm&lt;/code&gt;. The LLM is instructed to respond with only the department name for reliable parsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telnyx-llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the LLM fails or returns "unknown," the app falls back to keyword matching. After &lt;code&gt;max_turns&lt;/code&gt; (3 by default), the call transfers to a default operator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speech gather with Call Control
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;gather_using_speech&lt;/code&gt; primitive plays a prompt and captures the caller's speech in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather_using_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;call_control_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;female-en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Webhook verification
&lt;/h2&gt;

&lt;p&gt;Every webhook request is verified with Ed25519 signature verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;construct_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TELNYX_PUBLIC_KEY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents spoofed requests from triggering call actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent state machine
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;IVRAgent&lt;/code&gt; class manages each call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;on_connect()&lt;/code&gt;: fetch menu config, generate LLM greeting, speak, gather&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;on_gather_ended(speech)&lt;/code&gt;: route intent via LLM, transfer or retry&lt;/li&gt;
&lt;li&gt;Tracks &lt;code&gt;turn_count&lt;/code&gt; and &lt;code&gt;max_turns&lt;/code&gt; before falling back to a default transfer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Management API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PUT /api/menu-config/&amp;lt;phone_number&amp;gt;&lt;/code&gt;: update KV menu config dynamically&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/menu-config/&amp;lt;phone_number&amp;gt;&lt;/code&gt;: retrieve current config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/agents&lt;/code&gt;: list active IVR agents (debugging)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /health&lt;/code&gt;: health check&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/voice-ivr-with-agent-backend
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill in your Telnyx API Key, Public Key, Connection ID, and default transfer number. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
ngrok http 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your Call Control Application webhook to &lt;code&gt;https://&amp;lt;your-ngrok-url&amp;gt;.ngrok-free.app/webhooks/voice&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;Before using with real callers, add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persistent KV store (Redis or database instead of in-memory dict)&lt;/li&gt;
&lt;li&gt;caller ID personalization for greetings&lt;/li&gt;
&lt;li&gt;call queues for hold instead of blind transfer&lt;/li&gt;
&lt;li&gt;auth on management endpoints&lt;/li&gt;
&lt;li&gt;monitoring for LLM latency and fallback rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Call Control docs: &lt;a href="https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AI Inference docs: &lt;a href="https://developers.telnyx.com/docs/ai/ai-overview" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/ai/ai-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Webhooks docs: &lt;a href="https://developers.telnyx.com/docs/develop/webhooks" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/develop/webhooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Python SDK: &lt;a href="https://github.com/team-telnyx/telnyx-python" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-python&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI on GitHub: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>telnyx</category>
      <category>python</category>
      <category>voice</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build a Natural Language IVR with Telnyx Call Control and AI Inference</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Fri, 28 Aug 2026 21:12:30 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-natural-language-ivr-with-telnyx-call-control-and-ai-inference-24l5</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-natural-language-ivr-with-telnyx-call-control-and-ai-inference-24l5</guid>
      <description>&lt;p&gt;Nobody likes phone trees. "Press 1 for billing, press 2 for support." Miss an option? Start over. It is friction at its worst.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;voice-ivr-with-agent-backend&lt;/code&gt; example replaces that with a natural language conversation. Callers just say what they need, and the app routes them to the right department.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it builds
&lt;/h2&gt;

&lt;p&gt;A Python/Flask app that handles inbound calls with a conversational IVR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbound Call
    -&amp;gt; answer with Call Control
    -&amp;gt; look up menu config from KV
    -&amp;gt; LLM generates a dynamic greeting
    -&amp;gt; gather(speech) — caller says what they need
    -&amp;gt; LLM routes intent to a department
    -&amp;gt; transfer call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The core primitives
&lt;/h2&gt;

&lt;p&gt;The app combines four Telnyx primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Call Control&lt;/strong&gt;: &lt;code&gt;answer()&lt;/code&gt;, &lt;code&gt;speak()&lt;/code&gt;, &lt;code&gt;gather_using_speech()&lt;/code&gt;, &lt;code&gt;transfer()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Inference&lt;/strong&gt;: &lt;code&gt;telnyx.ai.openai.chat.completions.create()&lt;/code&gt; for greetings and intent routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV store&lt;/strong&gt;: menu config per phone number (business name, departments, transfer numbers, keywords)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent state machine&lt;/strong&gt;: an &lt;code&gt;IVRAgent&lt;/code&gt; class that tracks call state, turn count, and retry logic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dynamic greeting via LLM
&lt;/h2&gt;

&lt;p&gt;Instead of a hardcoded "Press 1 for billing," the app generates a conversational greeting from the KV config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_dynamic_menu_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menu_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;departments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;menu_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;departments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;dept_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an IVR assistant for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;menu_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;business_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Available departments:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dept_list&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Greet the caller briefly and ask how you can help. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keep it conversational and under 2 sentences.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM generates the greeting through the OpenAI-compatible Telnyx Inference binding. If it fails, the app falls back to a static greeting from the KV config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent routing via LLM
&lt;/h2&gt;

&lt;p&gt;When the caller speaks, the transcription is passed to &lt;code&gt;route_intent_with_llm&lt;/code&gt;. The LLM is instructed to respond with only the department name for reliable parsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telnyx-llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the LLM fails or returns "unknown," the app falls back to keyword matching. After &lt;code&gt;max_turns&lt;/code&gt; (3 by default), the call transfers to a default operator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speech gather with Call Control
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;gather_using_speech&lt;/code&gt; primitive plays a prompt and captures the caller's speech in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather_using_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;call_control_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;female-en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Webhook verification
&lt;/h2&gt;

&lt;p&gt;Every webhook request is verified with Ed25519 signature verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;telnyx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;construct_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TELNYX_PUBLIC_KEY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents spoofed requests from triggering call actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent state machine
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;IVRAgent&lt;/code&gt; class manages each call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;on_connect()&lt;/code&gt;: fetch menu config, generate LLM greeting, speak, gather&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;on_gather_ended(speech)&lt;/code&gt;: route intent via LLM, transfer or retry&lt;/li&gt;
&lt;li&gt;Tracks &lt;code&gt;turn_count&lt;/code&gt; and &lt;code&gt;max_turns&lt;/code&gt; before falling back to a default transfer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Management API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PUT /api/menu-config/&amp;lt;phone_number&amp;gt;&lt;/code&gt;: update KV menu config dynamically&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/menu-config/&amp;lt;phone_number&amp;gt;&lt;/code&gt;: retrieve current config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/agents&lt;/code&gt;: list active IVR agents (debugging)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /health&lt;/code&gt;: health check&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/voice-ivr-with-agent-backend
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill in your Telnyx API Key, Public Key, Connection ID, and default transfer number. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
ngrok http 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your Call Control Application webhook to &lt;code&gt;https://&amp;lt;your-ngrok-url&amp;gt;.ngrok-free.app/webhooks/voice&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;Before using with real callers, add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persistent KV store (Redis or database instead of in-memory dict)&lt;/li&gt;
&lt;li&gt;caller ID personalization for greetings&lt;/li&gt;
&lt;li&gt;call queues for hold instead of blind transfer&lt;/li&gt;
&lt;li&gt;auth on management endpoints&lt;/li&gt;
&lt;li&gt;monitoring for LLM latency and fallback rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/voice-ivr-with-agent-backend&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Call Control docs: &lt;a href="https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/voice/programmable-voice/call-control-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AI Inference docs: &lt;a href="https://developers.telnyx.com/docs/ai/ai-overview" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/ai/ai-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Webhooks docs: &lt;a href="https://developers.telnyx.com/docs/develop/webhooks" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/develop/webhooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Python SDK: &lt;a href="https://github.com/team-telnyx/telnyx-python" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-python&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI on GitHub: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>telnyx</category>
      <category>python</category>
      <category>voice</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build a Shipment Agent That Texts Customers When Packages Move</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:17:45 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-shipment-agent-that-texts-customers-when-packages-move-5fp2</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-a-shipment-agent-that-texts-customers-when-packages-move-5fp2</guid>
      <description>&lt;p&gt;Package tracking usually feels backwards.&lt;/p&gt;

&lt;p&gt;The customer gets a tracking number, opens a page, refreshes it, waits, refreshes again, and tries to decode carrier status updates. Even when a business sends SMS updates, those messages are usually one-way.&lt;/p&gt;

&lt;p&gt;I wanted to build the opposite pattern: a shipment that can talk.&lt;/p&gt;

&lt;p&gt;This example turns each shipment into a small agent. It receives carrier updates, remembers package state, sends proactive SMS messages, handles customer replies, and exposes a voice/call path for the same shipment context.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the app does
&lt;/h2&gt;

&lt;p&gt;The app is a Python Flask service built around a &lt;code&gt;ShipmentAgent&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Each agent owns the state for one shipment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shipment ID&lt;/li&gt;
&lt;li&gt;current carrier status&lt;/li&gt;
&lt;li&gt;ETA&lt;/li&gt;
&lt;li&gt;customer contact&lt;/li&gt;
&lt;li&gt;event history&lt;/li&gt;
&lt;li&gt;scheduled follow-ups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something changes, the agent wakes up and decides what to do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Carrier status update
  -&amp;gt; POST /webhooks/carrier
  -&amp;gt; ShipmentAgent updates package state
  -&amp;gt; Telnyx Messaging sends the customer an SMS

Customer SMS reply
  -&amp;gt; POST /webhooks/telnyx
  -&amp;gt; verified Telnyx webhook
  -&amp;gt; ShipmentAgent responds with shipment context

Customer call
  -&amp;gt; POST /api/agents/&amp;lt;shipment_id&amp;gt;/call
  -&amp;gt; ShipmentAgent returns the current shipment summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting idea is not just "send a text when a package moves." The interesting idea is that the package has a durable communication object behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this pattern matters
&lt;/h2&gt;

&lt;p&gt;Most support automations are built around sessions.&lt;/p&gt;

&lt;p&gt;A chat session starts, a user asks a question, the bot responds, and then the session disappears.&lt;/p&gt;

&lt;p&gt;Shipments do not work that way. A package may be created today, delayed tomorrow, delivered three days from now, and followed up on next week. The agent needs to survive across events, channels, and time.&lt;/p&gt;

&lt;p&gt;That is why this example treats the agent as the package.&lt;/p&gt;

&lt;p&gt;Instead of asking "what should this webhook do?" the app asks "what happened to this shipment, and what should its agent do now?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Carrier updates become customer messages
&lt;/h2&gt;

&lt;p&gt;The carrier webhook accepts statuses like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PICKED_UP
IN_TRANSIT
DELAYED
OUT_FOR_DELIVERY
DELIVERED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app maps those carrier statuses into agent events.&lt;/p&gt;

&lt;p&gt;For example, when a shipment is picked up, the agent sets its status to &lt;code&gt;IN_TRANSIT&lt;/code&gt;, calculates an ETA, and sends the customer an SMS through Telnyx Messaging.&lt;/p&gt;

&lt;p&gt;When the package is delivered, the agent sends a delivery message and schedules a follow-up request.&lt;/p&gt;

&lt;p&gt;For this demo, state is stored in memory and scheduling uses a background thread. In production, you would move that state into Postgres, Redis, or another durable store, and use a real job runner for future wakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The webhook fix that matters
&lt;/h2&gt;

&lt;p&gt;The latest update to this example tightened the inbound SMS path.&lt;/p&gt;

&lt;p&gt;That matters because customer replies only work if the Telnyx webhook is verified and parsed correctly.&lt;/p&gt;

&lt;p&gt;The app now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sets the Telnyx public key on the SDK module at startup&lt;/li&gt;
&lt;li&gt;reads the correct &lt;code&gt;Telnyx-Timestamp&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;verifies incoming webhooks with &lt;code&gt;telnyx.Webhook.construct_event&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;handles the Telnyx v2 message payload shape&lt;/li&gt;
&lt;li&gt;supports &lt;code&gt;from_&lt;/code&gt; when the SDK maps the JSON &lt;code&gt;from&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;pins the Telnyx Python SDK to &lt;code&gt;telnyx&amp;gt;=2.0,&amp;lt;3.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is easy to overlook. The example uses the v2 SDK API shape, including &lt;code&gt;telnyx.Message.create&lt;/code&gt; and &lt;code&gt;telnyx.Webhook.construct_event&lt;/code&gt;, so the dependency is pinned to avoid accidentally installing a newer incompatible SDK.&lt;/p&gt;

&lt;p&gt;Small fix, big difference: inbound SMS replies can now verify correctly and route back to the right shipment agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup
&lt;/h2&gt;

&lt;p&gt;Clone the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/shipment-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELNYX_API_KEY=&amp;lt;your_telnyx_api_key&amp;gt;
TELNYX_PUBLIC_KEY=&amp;lt;your_telnyx_public_key&amp;gt;
TELNYX_MESSAGING_PROFILE_ID=&amp;lt;your_messaging_profile_id&amp;gt;
TELNYX_FROM_NUMBER=&amp;lt;your_telnyx_number&amp;gt;
TELNYX_TO_NUMBER=&amp;lt;customer_test_number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app starts on port &lt;code&gt;5000&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example carrier event
&lt;/h2&gt;

&lt;p&gt;You can simulate a package delay with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5000/webhooks/carrier &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "shipment_id": "SHP-DEMO-001",
    "status": "DELAYED",
    "event_data": {
      "new_eta": "2026-09-04"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app finds or creates the shipment agent, updates the package status, and sends the customer an SMS update.&lt;/p&gt;

&lt;p&gt;You can inspect the agent state with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:5000/api/agents/SHP-DEMO-001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where AI fits in
&lt;/h2&gt;

&lt;p&gt;When a customer replies by SMS, the app builds a prompt from the shipment context and the customer's message.&lt;/p&gt;

&lt;p&gt;That is the right place to use Telnyx AI Inference for natural language shipment Q&amp;amp;A: "Can I change the delivery instructions?", "Why is this delayed?", or "What happens if I am not home?"&lt;/p&gt;

&lt;p&gt;The important part is that the AI response is grounded in the shipment state the agent already owns.&lt;/p&gt;

&lt;p&gt;It is not a generic chatbot. It is answering as the communication layer for a specific package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;Before turning this into a real logistics workflow, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persistent storage for shipment agents&lt;/li&gt;
&lt;li&gt;idempotency for duplicate carrier webhooks&lt;/li&gt;
&lt;li&gt;retry handling for SMS delivery&lt;/li&gt;
&lt;li&gt;customer authentication before changing delivery preferences&lt;/li&gt;
&lt;li&gt;audit logs for agent decisions&lt;/li&gt;
&lt;li&gt;a real background job queue&lt;/li&gt;
&lt;li&gt;escalation to a human support workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But as a developer example, the core pattern is useful: model real-world objects as durable agents, then give those agents communication channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code example: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI toolkit: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Messaging docs: &lt;a href="https://developers.telnyx.com/docs/messaging" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/messaging&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Call Control docs: &lt;a href="https://developers.telnyx.com/docs/voice/programmable-voice" rel="noopener noreferrer"&gt;https://developers.telnyx.com/docs/voice/programmable-voice&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Webhook signing: &lt;a href="https://developers.telnyx.com/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing" rel="noopener noreferrer"&gt;https://developers.telnyx.com/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Portal: &lt;a href="https://portal.telnyx.com" rel="noopener noreferrer"&gt;https://portal.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>flask</category>
      <category>telnyx</category>
    </item>
    <item>
      <title>Build an AI Shipment Agent with SMS, Voice, and Telnyx Inference</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Thu, 27 Aug 2026 18:02:12 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-shipment-agent-with-sms-voice-and-telnyx-inference-4o4g</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-shipment-agent-with-sms-voice-and-telnyx-inference-4o4g</guid>
      <description>&lt;p&gt;Most package tracking flows make the customer do the work.&lt;/p&gt;

&lt;p&gt;You get a tracking number. You open a page. You refresh it. Maybe you get a generic text that says the package is out for delivery. If you need to ask a real question, you usually end up somewhere else entirely.&lt;/p&gt;

&lt;p&gt;I wanted to build the opposite shape: what if the package itself had an agent?&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;shipment-agent&lt;/code&gt; example is a Python and Flask app that treats a shipment as a durable AI entity. It can send proactive SMS updates, understand customer replies with Telnyx AI Inference, and answer inbound calls with shipment context.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it builds
&lt;/h2&gt;

&lt;p&gt;The app centers around a &lt;code&gt;ShipmentAgent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shipment status&lt;/li&gt;
&lt;li&gt;carrier and tracking context&lt;/li&gt;
&lt;li&gt;customer phone number&lt;/li&gt;
&lt;li&gt;interaction history&lt;/li&gt;
&lt;li&gt;messaging and voice behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of a stateless chatbot waiting in a web page, the agent lives alongside the shipment lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Carrier update
  -&amp;gt; Flask webhook
  -&amp;gt; ShipmentAgent updates state
  -&amp;gt; SMS customer

Customer SMS reply
  -&amp;gt; Telnyx Messaging webhook
  -&amp;gt; AI Inference response
  -&amp;gt; SMS reply

Customer phone call
  -&amp;gt; Telnyx Call Control
  -&amp;gt; ShipmentAgent answers with context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this is useful
&lt;/h2&gt;

&lt;p&gt;Shipment status is not just data. It is a customer communication problem.&lt;/p&gt;

&lt;p&gt;People want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is my package delayed?&lt;/li&gt;
&lt;li&gt;Can I leave delivery instructions?&lt;/li&gt;
&lt;li&gt;Did it already arrive?&lt;/li&gt;
&lt;li&gt;Who do I call if something looks wrong?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional tracking pages are good at showing status, but not at handling conversation. This example shows how to turn the shipment into a small communications agent that can respond across SMS and voice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main flow
&lt;/h2&gt;

&lt;p&gt;When a carrier status changes, the app receives a webhook.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;out_for_delivery
delayed
delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ShipmentAgent&lt;/code&gt; updates its internal state and sends a message to the customer through Telnyx Messaging.&lt;/p&gt;

&lt;p&gt;If the customer replies, the app passes the message and shipment context to Telnyx AI Inference. That lets the response include the current shipment state instead of acting like a generic support bot.&lt;/p&gt;

&lt;p&gt;If the customer calls the Telnyx number, the Call Control flow can answer with the same context.&lt;/p&gt;

&lt;p&gt;That is the key idea: SMS and voice are not separate experiences. They are two ways to talk to the same shipment agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telnyx pieces used
&lt;/h2&gt;

&lt;p&gt;The example uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telnyx Messaging for proactive SMS updates and customer replies&lt;/li&gt;
&lt;li&gt;Telnyx Call Control for inbound voice interactions&lt;/li&gt;
&lt;li&gt;Telnyx AI Inference for natural language processing&lt;/li&gt;
&lt;li&gt;Telnyx webhook signing to verify inbound events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Webhook verification matters here because the agent updates customer-facing shipment state based on incoming events. You want to know those events are authentic before they change what the agent does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the example
&lt;/h2&gt;

&lt;p&gt;Clone the examples repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/shipment-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill in the required Telnyx values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TELNYX_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_FROM_NUMBER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_MESSAGING_PROFILE_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_PUBLIC_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_TO_NUMBER&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app starts a Flask server locally, and you can connect the webhook URLs to your Telnyx messaging and voice configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would add for production
&lt;/h2&gt;

&lt;p&gt;The sample is intentionally small. For a real logistics workflow, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persistent database storage&lt;/li&gt;
&lt;li&gt;idempotency for carrier events&lt;/li&gt;
&lt;li&gt;richer delivery instruction handling&lt;/li&gt;
&lt;li&gt;customer authentication&lt;/li&gt;
&lt;li&gt;escalation to a human support team&lt;/li&gt;
&lt;li&gt;retries for SMS and webhook processing&lt;/li&gt;
&lt;li&gt;observability around missed or delayed events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the architecture is the useful part. A shipment can become a durable communications object, not just a tracking number on a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code example: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI toolkit: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx docs: &lt;a href="https://developers.telnyx.com" rel="noopener noreferrer"&gt;https://developers.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Portal: &lt;a href="https://portal.telnyx.com" rel="noopener noreferrer"&gt;https://portal.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>voice</category>
      <category>telnyx</category>
    </item>
    <item>
      <title>Build an AI Outbound Call Campaign with Python and Telnyx</title>
      <dc:creator>Sonam</dc:creator>
      <pubDate>Wed, 26 Aug 2026 21:58:10 +0000</pubDate>
      <link>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-outbound-call-campaign-with-python-and-telnyx-3hge</link>
      <guid>https://dev.to/sonam_50a41a4ced7e6b4f3fa/build-an-ai-outbound-call-campaign-with-python-and-telnyx-3hge</guid>
      <description>&lt;p&gt;Outbound call campaigns look simple until you need them to behave like real software.&lt;/p&gt;

&lt;p&gt;Calling one number is easy. Calling a list of numbers means you suddenly care about queueing, rate limits, call state, webhook verification, result tracking, and summaries for the team.&lt;/p&gt;

&lt;p&gt;I built a small example for that pattern: an AI Call Campaign Orchestrator using Python, Flask, Telnyx Call Control, the Telnyx Agent SDK, SQLite, and SMS.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-call-campaign-orchestrator" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-call-campaign-orchestrator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the app does
&lt;/h2&gt;

&lt;p&gt;The app exposes a simple API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /campaign&lt;/code&gt; starts a campaign from a list of phone numbers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /campaign/&amp;lt;campaign_id&amp;gt;&lt;/code&gt; returns campaign status and call results&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /webhooks/call&lt;/code&gt; receives Telnyx Call Control events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /health&lt;/code&gt; confirms the service is running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part is what happens after you submit the campaign.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;CampaignAgent&lt;/code&gt; queues the calls, applies a rate limit, places outbound calls through Call Control, stores results in SQLite, and sends an SMS summary when the campaign is complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /campaign
  -&amp;gt; queue calls
  -&amp;gt; rate-limit delivery
  -&amp;gt; place calls with Call Control
  -&amp;gt; receive signed webhooks
  -&amp;gt; store results in SQLite
  -&amp;gt; send SMS summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Starting a campaign
&lt;/h2&gt;

&lt;p&gt;You send a list of E.164 phone numbers to &lt;code&gt;POST /campaign&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5000/campaign &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "phone_numbers": [
      "+15551234567",
      "+15557654321",
      "+15559876543"
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns a campaign ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"campaign_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3f2c1a5e-8b7d-4e6f-9a0c-1d2e3f4a5b6c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"started"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the agent owns the campaign lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rate limiting matters
&lt;/h2&gt;

&lt;p&gt;Outbound communication workflows need pacing.&lt;/p&gt;

&lt;p&gt;If you fire too many calls at once, you can create a bad user experience and run into carrier or operational limits. This sample uses &lt;code&gt;CALL_RATE_LIMIT_PER_MINUTE&lt;/code&gt; so campaign delivery is controlled from configuration instead of buried inside the call loop.&lt;/p&gt;

&lt;p&gt;That makes the campaign behavior easier to tune without rewriting the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Placing calls with Call Control
&lt;/h2&gt;

&lt;p&gt;For each queued number, the app places an outbound Call Control call with the Telnyx Python SDK.&lt;/p&gt;

&lt;p&gt;The app needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TELNYX_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_CONNECTION_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_PHONE_NUMBER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_WEBHOOK_URL&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The webhook URL points back to the app, so call lifecycle events can update campaign state as calls are answered, completed, or fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying webhooks
&lt;/h2&gt;

&lt;p&gt;The webhook endpoint uses &lt;code&gt;telnyx.webhooks.unwrap()&lt;/code&gt; with the Telnyx Ed25519 public key.&lt;/p&gt;

&lt;p&gt;That matters because webhook handlers should not blindly trust incoming HTTP requests. The app verifies that the event really came from Telnyx before it updates call state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Telnyx Call Control event
  -&amp;gt; POST /webhooks/call
  -&amp;gt; verify signature
  -&amp;gt; update campaign result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tracking status
&lt;/h2&gt;

&lt;p&gt;At any point, you can poll:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:5000/campaign/&amp;lt;campaign_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes total calls, completion status, and per-number results.&lt;/p&gt;

&lt;p&gt;That gives you a simple campaign dashboard API without adding another database service just for the sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop with SMS
&lt;/h2&gt;

&lt;p&gt;When the campaign finishes, the app sends a summary SMS using Telnyx Messaging.&lt;/p&gt;

&lt;p&gt;You configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TELNYX_SMS_FROM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TELNYX_SMS_TO&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a demo, this is a clean way to prove the workflow completed. In a production version, the same step could also notify Slack, update a CRM, or send a report to another internal system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like about this pattern
&lt;/h2&gt;

&lt;p&gt;The app is not trying to be a full campaign product.&lt;/p&gt;

&lt;p&gt;It is showing the backend shape you need for one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one API to start the work&lt;/li&gt;
&lt;li&gt;an agent to manage queueing and pacing&lt;/li&gt;
&lt;li&gt;Call Control for the voice workflow&lt;/li&gt;
&lt;li&gt;signed webhooks for lifecycle updates&lt;/li&gt;
&lt;li&gt;SQL for campaign state&lt;/li&gt;
&lt;li&gt;SMS for completion notification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the useful part. It gives you the skeleton for a durable communications workflow without starting from a pile of disconnected services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;Clone the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/ai-call-campaign-orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure your environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fill in your Telnyx API key, connection ID, phone number, webhook URL, public key, SMS settings, and call rate limit.&lt;/p&gt;

&lt;p&gt;Run the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code example: &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-call-campaign-orchestrator" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-call-campaign-orchestrator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx AI toolkit: &lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;https://github.com/team-telnyx/ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx docs: &lt;a href="https://developers.telnyx.com" rel="noopener noreferrer"&gt;https://developers.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Telnyx Portal: &lt;a href="https://portal.telnyx.com" rel="noopener noreferrer"&gt;https://portal.telnyx.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>voice</category>
      <category>telnyx</category>
    </item>
  </channel>
</rss>
