<?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: Gunnar Grosch</title>
    <description>The latest articles on DEV Community by Gunnar Grosch (@gunnargrosch).</description>
    <link>https://dev.to/gunnargrosch</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%2F348349%2Fc05198cd-00ab-474f-819d-3836aeb11553.jpg</url>
      <title>DEV Community: Gunnar Grosch</title>
      <link>https://dev.to/gunnargrosch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gunnargrosch"/>
    <language>en</language>
    <item>
      <title>Build, Operate, and Automate Sinch APIs with the Sinch CLI</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:20:15 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/build-operate-and-automate-sinch-apis-with-the-sinch-cli-16k3</link>
      <guid>https://dev.to/gunnargrosch/build-operate-and-automate-sinch-apis-with-the-sinch-cli-16k3</guid>
      <description>&lt;p&gt;The Sinch CLI (&lt;code&gt;@sinch/cli&lt;/code&gt;) puts the Sinch platform in one terminal command: &lt;code&gt;sinch&lt;/code&gt;. You can get a phone number, route calls to a &lt;a href="https://developers.sinch.com/docs/voice-2.0" rel="noopener noreferrer"&gt;Voice API v2&lt;/a&gt; service, build and deploy a Sinch Function, send a message over the Sinch Conversation API, and then script the same steps in CI with &lt;code&gt;--json&lt;/code&gt; output and predictable exit codes.&lt;/p&gt;

&lt;p&gt;The same commands work for coding agents. Claude Code, Kiro, Cursor, and GitHub Copilot all work through a terminal, so they can run &lt;code&gt;sinch&lt;/code&gt; just like you do. They use &lt;code&gt;--help&lt;/code&gt; to find commands, &lt;code&gt;--json&lt;/code&gt; to read structured output, and exit codes to decide what to do next, without assembling raw API requests. Ask your agent to rent a number and point it at your function, and it can do that with the commands in this post.&lt;/p&gt;

&lt;p&gt;This post walks one path from start to finish. You'll get a number, create a Voice service, scaffold a voice function, test it locally, deploy it, and place a call that speaks to you. After that comes messaging, the rest of the products the CLI covers, automation, and how to give your coding agent Sinch-specific skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  You'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 24 or newer&lt;/li&gt;
&lt;li&gt;A Sinch account and a project in the &lt;a href="https://dashboard.sinch.com" rel="noopener noreferrer"&gt;Sinch Build Dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A project ID, access key ID, and access key secret from the project's &lt;strong&gt;Access Keys&lt;/strong&gt; page&lt;/li&gt;
&lt;li&gt;A Conversation App with at least one channel configured, for the messaging section&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a development project for this walkthrough. The steps below repoint a Voice service's webhook, and you don't want that happening to a service that takes real customer calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and authenticate
&lt;/h2&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;span class="nt"&gt;-g&lt;/span&gt; @sinch/cli
sinch auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;auth login&lt;/code&gt; asks for your project ID, access key ID, and access key secret, then verifies the key pair. That one login covers &lt;a href="https://developers.sinch.com/docs/numbers" rel="noopener noreferrer"&gt;Numbers&lt;/a&gt;, Voice API v2, Functions, the Sinch Conversation API, fax, SIP trunking, and porting.&lt;/p&gt;

&lt;p&gt;Credentials go into your OS keychain: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Short-lived session tokens go in &lt;code&gt;~/.sinch/&lt;/code&gt;. Check what's stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch auth status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get a phone number
&lt;/h2&gt;

&lt;p&gt;Search for a number with voice capability, rent it, and confirm it's active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers available search &lt;span class="nt"&gt;--region&lt;/span&gt; US &lt;span class="nt"&gt;--type&lt;/span&gt; LOCAL &lt;span class="nt"&gt;--capabilities&lt;/span&gt; SMS VOICE
sinch numbers available rent +12025550134
sinch numbers active list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;active list&lt;/code&gt; shows each number with its capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+1 202-555-0134  [US] LOCAL  SMS, VOICE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some regions need supporting documents, such as proof of address or business identity, before a number can go live. Search results tag those numbers &lt;code&gt;[docs required]&lt;/code&gt;. If a rental needs documents the CLI switches to a guided KYC order and runs that region's questionnaire, which it fetches from the API at run time. You can also start an order directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers order create &lt;span class="nt"&gt;--region&lt;/span&gt; DE &lt;span class="nt"&gt;--type&lt;/span&gt; LOCAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create a Voice service
&lt;/h2&gt;

&lt;p&gt;Voice API v2 &lt;a href="https://developers.sinch.com/docs/voice-2.0/getting-started" rel="noopener noreferrer"&gt;routes inbound calls&lt;/a&gt; through a &lt;strong&gt;service&lt;/strong&gt;. A service holds one setting that matters here: The webhook URL that receives call events. List what your project already has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice services list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list marks one service as the project default. Calls you place with &lt;code&gt;sinch voice calls create&lt;/code&gt; run through the default service, so for this walkthrough use that one. If the project has no services yet, create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice services create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Support line"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI prints the new service ID, a &lt;code&gt;sinch voice services update&lt;/code&gt; command for setting the webhook, and the &lt;code&gt;VOICE_SERVICE_ID=&amp;lt;id&amp;gt;&lt;/code&gt; line that routes a function to the service. You don't need to set a webhook yourself: The function steps below do it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a voice function
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/quickstart" rel="noopener noreferrer"&gt;Sinch Functions&lt;/a&gt; run your call-handling code on Sinch. The CLI covers the whole loop: Scaffold, test locally, deploy, and watch the logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions init simple-voice-ivr &lt;span class="nt"&gt;--name&lt;/span&gt; my-function
&lt;span class="nb"&gt;cd &lt;/span&gt;my-function
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; uses the Node.js runtime for this template (add &lt;code&gt;--runtime csharp&lt;/code&gt; for C#) and asks four questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Company name.&lt;/strong&gt; This goes into the voice prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice service.&lt;/strong&gt; This is where the function receives calls. Pick the default service from the previous step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-configure voice integration on deploy.&lt;/strong&gt; Say yes. This is what lets &lt;code&gt;deploy&lt;/code&gt; point the service at your function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database.&lt;/strong&gt; Pick &lt;strong&gt;No database&lt;/strong&gt; for this example.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it installs dependencies and prints the next steps. Run &lt;code&gt;sinch templates list&lt;/code&gt; to see the rest of the catalog: IVRs, call bridging, number masking, SMS and RCS responders, and realtime AI voice agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make it speak on an outbound call
&lt;/h3&gt;

&lt;p&gt;The template's &lt;code&gt;voiceWebhook&lt;/code&gt; handles inbound calls with an &lt;code&gt;incoming&lt;/code&gt; handler that plays a menu. For outbound calls, add an &lt;code&gt;answered&lt;/code&gt; handler to the same &lt;code&gt;onCall&lt;/code&gt; in &lt;code&gt;function.ts&lt;/code&gt;:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voiceWebhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// incoming, manage, and completed from the template stay as they are&lt;/span&gt;
  &lt;span class="na"&gt;answered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your package is arriving today.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hangup&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 a call you place is answered, Voice API v2 sends a &lt;a href="https://developers.sinch.com/docs/functions/functions/concepts/voice-callbacks" rel="noopener noreferrer"&gt;&lt;code&gt;call.answered&lt;/code&gt; event&lt;/a&gt; to the service webhook. The function replies with the commands to run: Say the message, then hang up. That's why &lt;code&gt;calls create&lt;/code&gt; has no &lt;code&gt;--say&lt;/code&gt; flag. The function decides what happens on the call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test it locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dev&lt;/code&gt; builds the function, runs it on &lt;code&gt;localhost:3000&lt;/code&gt;, and reloads on every save. Send it the event Voice API v2 would send and look at the commands it returns:&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:3000/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"event":"call.answered","callId":"01TEST","sessionId":"01TEST"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SAY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"say"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your package is arriving today."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"voiceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Emma"&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;span class="p"&gt;}&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;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"onFinish"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hangup"&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;span class="nl"&gt;"onFailure"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hangup"&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;span class="p"&gt;}&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;span class="p"&gt;]&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;&lt;code&gt;dev --tunnel&lt;/code&gt; goes one step further. It opens a Sinch tunnel and points your Voice service's webhook at your laptop, so real calls reach the local server before you deploy anything. The service keeps pointing at the tunnel after you stop &lt;code&gt;dev&lt;/code&gt;, so run &lt;code&gt;deploy&lt;/code&gt; afterwards to move it back to the function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy it
&lt;/h3&gt;

&lt;p&gt;Stop &lt;code&gt;dev&lt;/code&gt; with &lt;code&gt;Ctrl+C&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;sinch functions deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy&lt;/code&gt; packages the source, audits the npm dependencies, and rolls it out. Because auto-configuration is on, it also points the Voice service webhook at the deployed function's URL. Check it with &lt;code&gt;sinch voice services list&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Place the call
&lt;/h3&gt;

&lt;p&gt;To see the request before it costs anything, add &lt;code&gt;--dry-run&lt;/code&gt;. It prints the exact request body without placing the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice calls create +15551234567 &lt;span class="nt"&gt;--from&lt;/span&gt; +12025550134 &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"callName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PHONE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+12025550134"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PHONE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+15551234567"&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;span class="p"&gt;}&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;span class="p"&gt;]&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;&lt;code&gt;bridge&lt;/code&gt;, &lt;code&gt;hangup&lt;/code&gt;, and &lt;code&gt;patch&lt;/code&gt; take &lt;code&gt;--dry-run&lt;/code&gt;, too. SIP destinations take the scheme, such as &lt;code&gt;sip:agent@pbx.example.com&lt;/code&gt;, and the dry run shows how they're sent. Drop the flag to place the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice calls create +15551234567 &lt;span class="nt"&gt;--from&lt;/span&gt; +12025550134
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use your own phone as the destination. &lt;code&gt;--from&lt;/code&gt; is the voice-capable number from earlier. Answer and you'll hear "Your package is arriving today."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;create&lt;/code&gt; prints a &lt;strong&gt;session ID&lt;/strong&gt;, the service that handled the call, and the command to look the call up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session ID: 01M3MWAGB1JPKE7VVBVN36BYG1
Service:    YOUR_SERVICE_ID

Inspect with: sinch voice calls list --service-id YOUR_SERVICE_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session ID is not a call ID. To inspect a single call, list the service's calls and use the ULID call ID from that list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice calls list &lt;span class="nt"&gt;--service-id&lt;/span&gt; YOUR_SERVICE_ID
sinch voice calls get YOUR_CALL_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call ID                     Dir  Type   From          To            Result     Duration
---------------------------------------------------------------------------------------
01M3MWAGB6HH85Y9GMT8V79D57  out  PHONE  +12025550134  +15551234567  COMPLETED  4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;calls list&lt;/code&gt; filters with &lt;code&gt;--call-result&lt;/code&gt;, &lt;code&gt;--call-type&lt;/code&gt;, &lt;code&gt;--from&lt;/code&gt;, &lt;code&gt;--to&lt;/code&gt;, &lt;code&gt;--start-time&lt;/code&gt;, and &lt;code&gt;--end-time&lt;/code&gt;, all optional and combinable. &lt;code&gt;sinch voice calls&lt;/code&gt; also has &lt;code&gt;bridge&lt;/code&gt; to connect two parties, &lt;code&gt;hangup&lt;/code&gt; to end legs of a live call, and &lt;code&gt;patch&lt;/code&gt; to add a party to a call that's already up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch the logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions logs &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--follow&lt;/code&gt; streams each request the function handles, with its response and any &lt;code&gt;console.log&lt;/code&gt; output. The call you just placed shows up as two events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time        Method   Status  Duration    URL
────────────────────────────────────────────────────────────────────────────────
22:47:02  POST    204  1ms        /webhook/voice [call.hangup]
22:46:59  POST    200  1ms        /webhook/voice [call.answered]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an interactive view where the arrow keys move between requests and Enter expands one, use &lt;code&gt;sinch functions logs --interactive&lt;/code&gt;. Filter with &lt;code&gt;--level&lt;/code&gt;, &lt;code&gt;--search&lt;/code&gt;, or &lt;code&gt;--since&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rest of the lifecycle: &lt;code&gt;functions list&lt;/code&gt;, &lt;code&gt;functions status&lt;/code&gt;, &lt;code&gt;functions download&lt;/code&gt; to pull the deployed source back down, &lt;code&gt;functions delete&lt;/code&gt;, and &lt;code&gt;functions docs&lt;/code&gt; to generate a README from the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets
&lt;/h3&gt;

&lt;p&gt;Secrets live in your OS keychain and go up with the deployment, so the value never sits in your project. Run this from the function directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch secrets add CRM_API_KEY YOUR_SECRET_VALUE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI stores the value in the keychain and adds an empty &lt;code&gt;CRM_API_KEY=&lt;/code&gt; line to the function's &lt;code&gt;.env&lt;/code&gt;. That empty line tells &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt; to fill the value in from the keychain. Your code reads it as &lt;code&gt;process.env.CRM_API_KEY&lt;/code&gt;, locally and in production.&lt;/p&gt;

&lt;p&gt;For a longer build on this same template, &lt;a href="https://sinch.com/blog/how-i-built-voice-ivr-sinch-functions/" rel="noopener noreferrer"&gt;Build a Voice IVR with Sinch Functions&lt;/a&gt; walks through the menu logic and a live phone call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stream call audio or connect the Voice Relay
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;calls create&lt;/code&gt; can also bridge the call to a WebSocket endpoint instead of a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice calls create +15551234567 &lt;span class="nt"&gt;--stream&lt;/span&gt; wss://example.com/audio
sinch voice calls create +15551234567 &lt;span class="nt"&gt;--relay&lt;/span&gt; wss://example.com/relay &lt;span class="nt"&gt;--tts-voice&lt;/span&gt; Emma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--stream&lt;/code&gt; sends the &lt;a href="https://developers.sinch.com/docs/voice-2.0/api-reference/stream" rel="noopener noreferrer"&gt;raw call audio&lt;/a&gt; to your endpoint. &lt;code&gt;--relay&lt;/code&gt; connects the &lt;a href="https://developers.sinch.com/docs/voice-2.0/tutorials/voice-relay" rel="noopener noreferrer"&gt;&lt;strong&gt;Voice Relay&lt;/strong&gt;&lt;/a&gt;, where Sinch runs the speech-to-text and text-to-speech and your endpoint only exchanges text. Both need a public &lt;code&gt;wss://&lt;/code&gt; endpoint. Add &lt;code&gt;--dry-run&lt;/code&gt; to see the full command list either one sends. To build on them, start from the &lt;code&gt;voice-relay-agent&lt;/code&gt;, &lt;code&gt;voice-relay-echo&lt;/code&gt;, or &lt;code&gt;realtime-*&lt;/code&gt; templates in &lt;code&gt;sinch templates list&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send an SMS with the Sinch Conversation API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch conversation send +15551234567 &lt;span class="s2"&gt;"Hello from Sinch"&lt;/span&gt; &lt;span class="nt"&gt;--channel&lt;/span&gt; SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;send&lt;/code&gt; goes through your Conversation App and supports eight message types with &lt;code&gt;--type&lt;/code&gt;: text, media, template, card, carousel, choice, location, and list. If the app doesn't have the channel you pass, the CLI lists the channels it does have. The rest of the &lt;a href="https://developers.sinch.com/docs/conversation" rel="noopener noreferrer"&gt;Sinch Conversation API&lt;/a&gt; is in the same group: &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;contacts&lt;/code&gt;, &lt;code&gt;conversations&lt;/code&gt;, &lt;code&gt;apps&lt;/code&gt;, &lt;code&gt;webhooks&lt;/code&gt;, and &lt;code&gt;templates&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More in the CLI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fax.&lt;/strong&gt; &lt;code&gt;sinch fax send --to +12025550134 --file ./document.pdf&lt;/code&gt; sends a fax, and &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;status --wait&lt;/code&gt;, and &lt;code&gt;download&lt;/code&gt; track it. Fax services, cover pages, and fax-to-email have their own subcommands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIP trunking.&lt;/strong&gt; &lt;code&gt;sinch sip&lt;/code&gt; manages &lt;code&gt;trunks&lt;/code&gt;, &lt;code&gt;endpoints&lt;/code&gt;, &lt;code&gt;acls&lt;/code&gt;, &lt;code&gt;credential-lists&lt;/code&gt;, &lt;code&gt;countries&lt;/code&gt;, and &lt;code&gt;calls&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Porting.&lt;/strong&gt; &lt;code&gt;sinch porting check +12025550134&lt;/code&gt; tells you whether a number can move to Sinch. &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;documents&lt;/code&gt;, and &lt;code&gt;activation&lt;/code&gt; handle the port-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice API v1.&lt;/strong&gt; Projects built on a Voice application use &lt;code&gt;sinch voice v1&lt;/code&gt;: &lt;code&gt;applications&lt;/code&gt;, &lt;code&gt;callouts&lt;/code&gt;, &lt;code&gt;calls&lt;/code&gt;, and &lt;code&gt;conferences&lt;/code&gt;. These commands authenticate with the Voice application key and secret. Set &lt;code&gt;SINCH_APPLICATION_KEY&lt;/code&gt; and &lt;code&gt;SINCH_APPLICATION_SECRET&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automate it
&lt;/h2&gt;

&lt;p&gt;Most commands take &lt;code&gt;--json&lt;/code&gt; and print a single JSON document with no table and no spinner, so the output pipes straight into &lt;code&gt;jq&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;sinch functions list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.functions[] | select(.status != "Running") | .name'&lt;/span&gt;
sinch functions deploy &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.url'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a pipeline, authenticate with environment variables instead of the keychain. They take precedence over stored credentials:&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;export &lt;/span&gt;&lt;span class="nv"&gt;SINCH_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT_ID
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SINCH_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_KEY_ID
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SINCH_KEY_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_KEY_SECRET
sinch functions deploy &lt;span class="nt"&gt;--non-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--non-interactive&lt;/code&gt; skips every prompt and uses defaults, and setting &lt;code&gt;CI=true&lt;/code&gt; has the same effect on &lt;code&gt;deploy&lt;/code&gt;. A non-interactive deploy is public unless you pass &lt;code&gt;--private&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Exit codes follow BSD &lt;code&gt;sysexits&lt;/code&gt; conventions, plus a Sinch range starting at 100. Two are worth branching on in a script, because retrying won't help with either:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;General failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authentication required: No credentials found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;101&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authentication failed: Credentials were rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Profiles let one install work across several projects. Create a profile, switch to it, and log in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch config profile create staging
sinch config profile use staging
sinch auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or leave the active profile alone and pass &lt;code&gt;--profile staging&lt;/code&gt; on a single command.&lt;/p&gt;

&lt;p&gt;Updates are opt-in. Turn them on with &lt;code&gt;sinch config set autoUpdate true&lt;/code&gt;, or check on demand with &lt;code&gt;sinch upgrade&lt;/code&gt;. Install shell completions with &lt;code&gt;sinch completion --install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the Sinch CLI with coding agents
&lt;/h2&gt;

&lt;p&gt;Every command in this post works the same when an agent runs it. These are the parts an agent relies on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--help&lt;/code&gt; on every command and group, to discover what exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--json&lt;/code&gt;, to read results as structured data&lt;/li&gt;
&lt;li&gt;Exit codes, to tell an auth problem from a failed call&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--non-interactive&lt;/code&gt;, to run without prompts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--dry-run&lt;/code&gt; on call commands, to check a request before it costs anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To give your assistant Sinch-specific knowledge, install Sinch's developer skills:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This installs skills from &lt;a href="https://github.com/sinch/skills" rel="noopener noreferrer"&gt;github.com/sinch/skills&lt;/a&gt; into your coding assistants. It includes a &lt;code&gt;sinch-cli&lt;/code&gt; skill that teaches the commands in this post, plus skills for Functions, Voice API v2, the Sinch Conversation API, Numbers, and the other Sinch APIs. After that you can describe what you need ("place a test call to my phone and show me the call log") and let the agent run the commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;calls get&lt;/code&gt; returns 404 for the ID from &lt;code&gt;create&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;create&lt;/code&gt; returns a session ID. &lt;code&gt;calls get&lt;/code&gt; takes the ULID call ID from &lt;code&gt;sinch voice calls list --service-id YOUR_SERVICE_ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbound calls stopped reaching your function after a &lt;code&gt;dev&lt;/code&gt; session.&lt;/strong&gt; The Voice service webhook still points at the tunnel. Run &lt;code&gt;sinch functions deploy&lt;/code&gt;, or set it by hand with &lt;code&gt;sinch voice services update YOUR_SERVICE_ID --webhook-url YOUR_FUNCTION_URL&lt;/code&gt;. Check where it points with &lt;code&gt;sinch voice services list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A secret is empty in production.&lt;/strong&gt; Check that &lt;code&gt;.env&lt;/code&gt; has the empty &lt;code&gt;CRM_API_KEY=&lt;/code&gt; line and that the key shows up in &lt;code&gt;sinch secrets list&lt;/code&gt;, then deploy again. &lt;code&gt;sinch secrets add&lt;/code&gt; only writes the &lt;code&gt;.env&lt;/code&gt; line when you run it from the function directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A SIP destination is rejected.&lt;/strong&gt; Voice API v2 wants the scheme: &lt;code&gt;sip:agent@pbx.example.com&lt;/code&gt; or &lt;code&gt;sips:&lt;/code&gt; for TLS. Run the command with &lt;code&gt;--dry-run&lt;/code&gt; to see what's sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;sinch voice v1&lt;/code&gt; command asks for credentials.&lt;/strong&gt; v1 uses the Voice application key and secret, not the project access key. Set &lt;code&gt;SINCH_APPLICATION_KEY&lt;/code&gt; and &lt;code&gt;SINCH_APPLICATION_SECRET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A script exits with &lt;code&gt;100&lt;/code&gt; or &lt;code&gt;101&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;100&lt;/code&gt; means no credentials were found and &lt;code&gt;101&lt;/code&gt; means they were rejected. Run &lt;code&gt;sinch auth status&lt;/code&gt;, or check the &lt;code&gt;SINCH_KEY_ID&lt;/code&gt; and &lt;code&gt;SINCH_KEY_SECRET&lt;/code&gt; values in your pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Command reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g @sinch/cli&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log in&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch auth login&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find a number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch numbers available search --region US --type LOCAL --capabilities VOICE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rent a number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch numbers available rent +12025550134&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List your numbers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch numbers active list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List Voice services&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch voice services list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a Voice service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch voice services create --name "Support line"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point a service at a URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch voice services update YOUR_SERVICE_ID --webhook-url YOUR_URL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaffold a function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch functions init simple-voice-ivr --name my-function&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run locally&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch functions dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route real calls to your laptop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch functions dev --tunnel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch functions deploy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch functions logs --follow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a secret&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch secrets add CRM_API_KEY YOUR_SECRET_VALUE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Place a call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch voice calls create +15551234567 --from +12025550134&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview a call request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch voice calls create +15551234567 --dry-run&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List calls&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch voice calls list --service-id YOUR_SERVICE_ID&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send an SMS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch conversation send +15551234567 "Hello" --channel SMS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send a fax&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch fax send --to +12025550134 --file ./document.pdf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check portability&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch porting check +12025550134&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch projects&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch config profile use staging&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install agent skills&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sinch skills install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have a number, a Voice service, a function that answers and places calls, and the commands to script all of it. &lt;a href="https://sinch.com/blog/how-i-built-voice-ivr-sinch-functions/" rel="noopener noreferrer"&gt;Build a Voice IVR with Sinch Functions&lt;/a&gt; goes deeper on the function side. An upcoming post builds an AI voice agent on &lt;code&gt;--stream&lt;/code&gt; and &lt;code&gt;--relay&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use the dashboard for configuration you set once. Use the CLI for everything you repeat, debug, or run in a pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions" rel="noopener noreferrer"&gt;Sinch Functions documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/cli/installation" rel="noopener noreferrer"&gt;Sinch CLI installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/cli" rel="noopener noreferrer"&gt;Sinch CLI reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/templates" rel="noopener noreferrer"&gt;Sinch Functions templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@sinch/cli" rel="noopener noreferrer"&gt;@sinch/cli on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sinch/skills" rel="noopener noreferrer"&gt;Sinch developer skills&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the first thing you'd script with the Sinch CLI? Let us know in the comments.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>api</category>
      <category>ai</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Build a voice IVR with Sinch Functions</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Tue, 29 Sep 2026 07:54:14 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/build-a-voice-ivr-with-sinch-functions-26io</link>
      <guid>https://dev.to/gunnargrosch/build-a-voice-ivr-with-sinch-functions-26io</guid>
      <description>&lt;p&gt;I have built plenty of voice and messaging integrations where a small HTTP service existed for one reason: receive a callback and turn it into the next action. The service was rarely complicated, but it still needed to be deployed, monitored, and kept available.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.sinch.com/docs/functions/" rel="noopener noreferrer"&gt;&lt;strong&gt;Sinch Functions&lt;/strong&gt;&lt;/a&gt; runs that callback code on Sinch infrastructure. You write a function, deploy it with the Sinch CLI, and Sinch routes voice and messaging traffic to it. Keep your customer database, order service, and other backend services where they already run. Functions handles the code triggered by communications events.&lt;/p&gt;

&lt;p&gt;I use a small &lt;a href="https://developers.sinch.com/docs/voice-2.0" rel="noopener noreferrer"&gt;Voice API v2&lt;/a&gt; IVR in this post because it makes the event flow easy to test from a phone. The same development and deployment model applies to messaging webhooks and other communications workflows: create the function, run it locally behind a temporary tunnel, test it with &lt;code&gt;curl&lt;/code&gt; and live traffic, then deploy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  You'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Sinch account and a project in the &lt;a href="https://dashboard.sinch.com" rel="noopener noreferrer"&gt;Sinch Build Dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A project ID, access key ID, and access key secret from the project's &lt;strong&gt;Access Keys&lt;/strong&gt; page&lt;/li&gt;
&lt;li&gt;Node.js 24 or newer&lt;/li&gt;
&lt;li&gt;A Voice API v2 service with a phone number assigned to it. Create one with &lt;code&gt;sinch voice services create&lt;/code&gt; and rent a number with &lt;code&gt;sinch numbers available rent&lt;/code&gt;, or let the &lt;code&gt;init&lt;/code&gt; picker create the service for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Renting a number and voice usage are billable; check your project's Billing Overview in the dashboard for current rates. You don't need a number to write and test the handler. The local &lt;code&gt;curl&lt;/code&gt; request below returns the commands without placing a call. The phone call at the end is what needs the Voice service and number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Functions fits this job
&lt;/h2&gt;

&lt;p&gt;A simple phone tree usually has a small control loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A caller reaches a Sinch phone number.&lt;/li&gt;
&lt;li&gt;Voice API v2 sends a &lt;code&gt;call.incoming&lt;/code&gt; event to your function.&lt;/li&gt;
&lt;li&gt;Your function returns commands to answer the call and start a menu.&lt;/li&gt;
&lt;li&gt;The platform plays the prompt, collects the keypress, and runs the commands in the matching branch. Your function is not called again for that step.&lt;/li&gt;
&lt;li&gt;When the call ends, Voice API v2 sends &lt;code&gt;call.hangup&lt;/code&gt;, and the &lt;code&gt;completed&lt;/code&gt; handler logs it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a menu declares what to do for every key, the branch runs on the Sinch platform instead of coming back to your code. That's one fewer network hop and one fewer function invocation per keypress, and it's what keeps a small IVR small.&lt;/p&gt;

&lt;p&gt;Sinch Functions provides the runtime, routing, logs, local tunnel, and deployment path for that part of the application. Sinch delivers the events to your code inside its own network, and the CLI wires the callback URL for you during local development and deployment. With the private deployment used in this tutorial, there's no public production webhook endpoint to expose and no separate deployment pipeline to maintain.&lt;/p&gt;

&lt;p&gt;The distinction matters. A function can call your existing APIs to look up an order or customer, but it shouldn't become a substitute for the system that owns that data. Keep the business system where it belongs and use Functions at the communications boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and authenticate the CLI
&lt;/h2&gt;

&lt;p&gt;Install the Sinch CLI globally:&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;span class="nt"&gt;-g&lt;/span&gt; @sinch/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authenticate with the project credentials from the Sinch Build Dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI prompts for your project ID, access key ID, and access key secret, and stores the secret in your OS credential store rather than in the project directory.&lt;/p&gt;

&lt;p&gt;Check the signed-in project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch auth status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voice functions use this project key pair. When you create one, the CLI lets you select a Voice service and writes its ID to &lt;code&gt;VOICE_SERVICE_ID&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the function
&lt;/h2&gt;

&lt;p&gt;Start from the built-in voice IVR template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions init simple-voice-ivr &lt;span class="nt"&gt;--name&lt;/span&gt; getting-started-functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template uses the Node.js runtime by default. To use C# instead, add &lt;code&gt;--runtime csharp&lt;/code&gt; to the command.&lt;/p&gt;

&lt;p&gt;The initializer is interactive. Complete these prompts before running another command:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter the company name to use in the voice prompts.&lt;/li&gt;
&lt;li&gt;Select the dedicated development Voice service from the picker, or enter its service ID manually.&lt;/li&gt;
&lt;li&gt;Accept &lt;strong&gt;Yes&lt;/strong&gt; to auto-configure the selected Voice service when you deploy.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;No database&lt;/strong&gt;. The Basic and Pro options provision SQLite storage, which this IVR does not need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The initializer extracts the template, configures secure credential access from your OS keychain, and installs dependencies. Wait for &lt;strong&gt;Function initialized successfully&lt;/strong&gt; and &lt;strong&gt;Dependencies installed successfully&lt;/strong&gt;, then move into the generated project:&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;cd &lt;/span&gt;getting-started-functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template installs its dependencies unless you pass &lt;code&gt;--skip-install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The files this tutorial touches are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getting-started-functions/
├── .env
├── function.ts
├── package.json
├── package-lock.json
├── tsconfig.json
├── sinch.json
├── runtime.json
└── test.http
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sinch.json&lt;/code&gt; contains the function name, runtime, and non-secret variables. &lt;code&gt;.env&lt;/code&gt; contains local configuration. Keep &lt;code&gt;.env&lt;/code&gt; out of source control. The template also includes &lt;code&gt;AGENTS.md&lt;/code&gt;, a &lt;code&gt;README.md&lt;/code&gt;, GitHub configuration, and VS Code debugging configuration, which this tutorial does not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the first handler
&lt;/h2&gt;

&lt;p&gt;The template generates a working IVR. We replace it with a smaller handler and build it back up so each part of the flow is visible.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;function.ts&lt;/code&gt; and replace the template code with this minimal handler:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onCall&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime/voice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voiceWebhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;call&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;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PHONE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;call&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;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Call received&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;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello! Thanks for calling.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hangup&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;&lt;code&gt;voiceWebhook&lt;/code&gt; is the named export that receives Voice API v2 events. &lt;code&gt;onCall&lt;/code&gt; selects a handler based on the event type and gives it a command builder. The local runtime dispatches a &lt;code&gt;call.*&lt;/code&gt; event posted to the function root, &lt;code&gt;POST /&lt;/code&gt;, to this handler.&lt;/p&gt;

&lt;p&gt;For an incoming call, &lt;code&gt;builder.answer()&lt;/code&gt; accepts the call, &lt;code&gt;say()&lt;/code&gt; turns the text into speech, and &lt;code&gt;hangup()&lt;/code&gt; ends it. The fluent builder returns valid Voice API commands without making you assemble the JSON manually.&lt;/p&gt;

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

&lt;p&gt;Start the development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server listens on port &lt;code&gt;3000&lt;/code&gt; and reloads TypeScript changes automatically. The CLI asks whether to enable the Sinch tunnel for external access. Choose &lt;strong&gt;Yes: Enable tunnel this time&lt;/strong&gt; for this walkthrough.&lt;/p&gt;

&lt;p&gt;While the tunnel is up, the CLI points the selected Voice service's callback URL at your machine, so real calls to that service reach your laptop. It lives only as long as &lt;code&gt;sinch functions dev&lt;/code&gt; is running. Use a development Voice service, not the one carrying customer calls. The CLI warns that real calls route to your machine until you stop the development server with &lt;code&gt;Ctrl+C&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can verify the handler without renting a number or placing a call:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/ &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;'{
    "event": "call.incoming",
    "call": {
      "callId": "01LOCALTEST0000000000000001",
      "sessionId": "01LOCALTEST0000000000000002",
      "from": {
        "type": "PHONE",
        "phone": { "number": "+15551234567" }
      },
      "to": {
        "type": "PHONE",
        "phone": { "number": "+15559876543" }
      },
      "direction": "INBOUND"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contains the commands the builder produced:&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;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SAY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"say"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello! Thanks for calling."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"voiceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Emma"&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;span class="p"&gt;}&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;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"onFinish"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hangup"&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;span class="nl"&gt;"onFailure"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hangup"&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;span class="p"&gt;}&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;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"caller"&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;The terminal also prints the log line from the handler, which is much easier to inspect than a production webhook log while you are still building the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call received { from: '+15551234567', callId: '01LOCALTEST0000000000000001' }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then call the phone number assigned to the Voice service. You should hear the greeting and the call should end.&lt;/p&gt;

&lt;p&gt;If port &lt;code&gt;3000&lt;/code&gt; is already in use, choose another one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For breakpoint debugging, use &lt;code&gt;--debug&lt;/code&gt;. The CLI detects VS Code, creates or reuses &lt;code&gt;.vscode/launch.json&lt;/code&gt;, and prompts you to press &lt;code&gt;F5&lt;/code&gt; to attach the debugger. Set breakpoints in the compiled &lt;code&gt;function.js&lt;/code&gt; file, then test the function to hit them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev &lt;span class="nt"&gt;--debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add a menu
&lt;/h2&gt;

&lt;p&gt;Answering a call is useful as a health check, but an IVR needs to collect an answer. Replace &lt;code&gt;function.ts&lt;/code&gt; with the following example:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUniversalConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onCall&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime/voice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voiceWebhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPANY_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme Corp&lt;/span&gt;&lt;span class="dl"&gt;'&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;callerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CALLER_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15550000000&lt;/span&gt;&lt;span class="dl"&gt;'&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&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;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;menu&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`Thank you for calling &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Press 1 for sales, 2 for support, or 0 for an operator.`&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeatPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Press 1 for sales, 2 for support, or 0 for an operator.&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="nf"&gt;inputTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeatCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connecting you to sales now.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dialPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15551110001&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callerId&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="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&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;commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connecting you to support.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dialPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15551110002&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callerId&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="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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;commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please hold for an operator.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dialPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15551110000&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callerId&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="nf"&gt;onFail&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sorry, we did not receive your selection. Goodbye.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hangup&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Call ended&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&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;Set the three destination numbers to the numbers each option should reach, and set &lt;code&gt;CALLER_ID&lt;/code&gt; to a Sinch number you own or are allowed to present as the caller ID. &lt;code&gt;menu()&lt;/code&gt; builds the prompt and handles DTMF, the tones a keypress sends. Each &lt;code&gt;match()&lt;/code&gt; branch returns the commands that run when the caller presses that key, and &lt;code&gt;onFail()&lt;/code&gt; covers a timeout or an unsupported key, so no caller is left on a silent call.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;completed&lt;/code&gt; handler runs after the call ends. It's a good place to record the call ID and outcome, or to notify another system that a call has completed.&lt;/p&gt;

&lt;p&gt;The values after each &lt;code&gt;getVariable()&lt;/code&gt; name are fallbacks. They keep the example runnable when no configuration has been set, so you do not need to duplicate them in &lt;code&gt;.env&lt;/code&gt; for this walkthrough. Define the non-sensitive variables there only when you want to change the greeting or caller ID without editing and redeploying the handler, or when each environment needs different values. Add them to the &lt;code&gt;.env&lt;/code&gt; file the initializer created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPANY_NAME=Acme Corp
CALLER_ID=+15550000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because every branch has a &lt;code&gt;match()&lt;/code&gt; or &lt;code&gt;onFail()&lt;/code&gt;, the platform resolves the keypress without calling your function again. Run the earlier &lt;code&gt;call.incoming&lt;/code&gt; &lt;code&gt;curl&lt;/code&gt; request again to inspect the generated menu safely. Its response contains &lt;code&gt;startMenu: "main"&lt;/code&gt;, the prompt, and the &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, and &lt;code&gt;2&lt;/code&gt; match branches, but it doesn't dial the placeholder numbers. Then test it by calling the number: You hear the prompt, press a key, and the matching branch runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a menu needs your code
&lt;/h2&gt;

&lt;p&gt;This is an optional alternative to the platform-resolved menu above. Do not make this change for the IVR you have just built. Use it only when the next step depends on a lookup, such as an order, calendar, or customer record.&lt;/p&gt;

&lt;p&gt;To use this pattern, replace the current &lt;code&gt;incoming&lt;/code&gt; handler with the one below and add &lt;code&gt;manage&lt;/code&gt; to the same &lt;code&gt;onCall()&lt;/code&gt; handler map. The menu has no &lt;code&gt;match()&lt;/code&gt; or &lt;code&gt;onFail()&lt;/code&gt;, so Voice API v2 sends the collected digits to &lt;code&gt;manage&lt;/code&gt; in a &lt;code&gt;call.menu&lt;/code&gt; event:&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;incoming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&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;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;menu&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Press 1 for sales, or 2 for support.&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="nf"&gt;repeatPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Press 1 for sales, or 2 for support.&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="nf"&gt;inputTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeatCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="nx"&gt;manage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;callerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CALLER_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15550000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connecting you to sales.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dialPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15551110001&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callerId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connecting you to support.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dialPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+15551110002&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callerId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nl"&gt;default&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid selection.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hangup&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;Use &lt;code&gt;manage&lt;/code&gt; when the branch has to check an order, a calendar, or a customer record before deciding where the call goes. Do not combine this menu with &lt;code&gt;match()&lt;/code&gt; or &lt;code&gt;onFail()&lt;/code&gt;: those handlers resolve the input on the platform, so &lt;code&gt;manage&lt;/code&gt; never runs.&lt;/p&gt;

&lt;p&gt;You can test this path without placing a call while &lt;code&gt;sinch functions dev&lt;/code&gt; is running:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/ &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;'{
    "event": "call.menu",
    "call": { "callId": "01LOCALTEST0000000000000001" },
    "menu": { "menuName": "main", "input": "1" }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That request reaches &lt;code&gt;manage&lt;/code&gt; with &lt;code&gt;call.menu.input&lt;/code&gt; set to &lt;code&gt;1&lt;/code&gt;. Without a &lt;code&gt;manage&lt;/code&gt; handler, the event has nowhere to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store secrets safely
&lt;/h2&gt;

&lt;p&gt;Put public configuration, such as &lt;code&gt;COMPANY_NAME&lt;/code&gt;, in &lt;code&gt;sinch.json&lt;/code&gt;. Sensitive values, such as the API token the operator route uses to look up the caller in your CRM, go in the secret store instead. Store the value outside the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch secrets add CRM_API_KEY YOUR_SECRET_VALUE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI adds &lt;code&gt;CRM_API_KEY=&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; for you. Read the value in the handler that needs it:&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;manage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;crmApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;CRM_API_KEY&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;crmApiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CRM_API_KEY is not configured&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="c1"&gt;// look up the caller with crmApiKey before routing&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard makes a missing credential explicit before the function tries to call the CRM. Secrets are redacted in function logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy the function
&lt;/h2&gt;

&lt;p&gt;When the local flow works, deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI validates the function first, then asks whether it should generate or update &lt;code&gt;README.md&lt;/code&gt;. Choose &lt;strong&gt;Yes: Generate documentation this time&lt;/strong&gt; for this walkthrough. It performs a dependency audit, packages the function, and prepares its configuration before prompting for a key.&lt;/p&gt;

&lt;p&gt;Choose the managed key. It's the default, and the CLI saves the choice in &lt;code&gt;sinch.json&lt;/code&gt;, so later deploys do not ask again. Choose the profile key only if you already manage function credentials yourself. The CLI then lists the configuration variables it will ship, marking secrets separately, and asks for the deployment access level. For an IVR that receives only Sinch voice callbacks, choose &lt;strong&gt;Private: Internal access only (Sinch services)&lt;/strong&gt;. You can change that default later with &lt;code&gt;--public&lt;/code&gt; or &lt;code&gt;--private&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before uploading, the CLI type-checks and bundles a Node.js function, then validates the package. It uploads the package over HTTPS and waits for the platform build and rollout. The resulting URL is displayed when the function is running.&lt;/p&gt;

&lt;p&gt;The CLI updates the callback URL for the Voice service selected during initialization. Call the assigned number again after deployment. You should hear the same menu without the local server or Cloudflare Tunnel running.&lt;/p&gt;

&lt;p&gt;Follow the production logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions logs &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log stream shows each request, response, handler logs, and any runtime warnings.&lt;/p&gt;

&lt;p&gt;You can also inspect a function's state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For integration with an existing operations stack, the Sinch Functions API exposes historical logs, streaming logs, metrics, and cost data. That's the path to use when the CLI's interactive logs are not enough for your production monitoring workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the deployed function
&lt;/h2&gt;

&lt;p&gt;Call the number assigned to the Voice service and work through the menu. That verifies the complete production path: number, Voice service, deployed function, and the menu branch. Keep &lt;code&gt;sinch functions logs --follow&lt;/code&gt; running in another terminal while you test so you can confirm the incoming event and response.&lt;/p&gt;

&lt;p&gt;If you intentionally deployed a public function, you can also repeat the earlier &lt;code&gt;call.incoming&lt;/code&gt; &lt;code&gt;curl&lt;/code&gt; request with the function URL printed after deployment in place of &lt;code&gt;http://localhost:3000&lt;/code&gt;. It verifies the deployed handler without placing a call. Do not change a private IVR to public only to run this test: use the assigned number for the private deployment path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The tunnel does not start:&lt;/strong&gt; Run &lt;code&gt;sinch functions dev --no-tunnel&lt;/code&gt; and post the local test payloads with &lt;code&gt;curl&lt;/code&gt;. That confirms the handler works while you sort out the network or firewall blocking the Cloudflare Tunnel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The test call does not reach your laptop:&lt;/strong&gt; Confirm that &lt;code&gt;sinch functions dev&lt;/code&gt; is still running and that you chose the Voice service assigned to the number you dialed. The tunnel updates that service's callback URL only while the development server is active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment stops because &lt;code&gt;.env&lt;/code&gt; is missing:&lt;/strong&gt; Run &lt;code&gt;sinch env init&lt;/code&gt;, then add any required secrets again with &lt;code&gt;sinch secrets add&lt;/code&gt;. The command restores secret names, not their values.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cleaning up
&lt;/h2&gt;

&lt;p&gt;Delete the function when you are done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions delete &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI displays the function details and asks you to confirm because deletion cannot be undone. Afterward, it removes the function ID from &lt;code&gt;sinch.json&lt;/code&gt;; run &lt;code&gt;sinch functions deploy&lt;/code&gt; to deploy the project again. &lt;code&gt;sinch functions list&lt;/code&gt; prints function IDs if you do not have one handy. Nothing else to clean up locally beyond deleting the project directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The IVR is deliberately small, but the same model applies to inbound SMS and Sinch Conversation API webhooks. A function can send messages through the &lt;a href="https://developers.sinch.com/docs/sdks" rel="noopener noreferrer"&gt;Sinch SDK clients&lt;/a&gt;, use the distributed cache for short-lived state, store durable files, or use the built-in replicated SQLite database when the interaction needs persistence.&lt;/p&gt;

&lt;p&gt;For voice, the command builder can also dial phone, SIP, stream, relay, and AI-agent destinations. Put the event-specific communications logic in the function, then call the service that owns your business data or workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/" rel="noopener noreferrer"&gt;Sinch Functions overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/cli/installation" rel="noopener noreferrer"&gt;Install the Sinch CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/guides/build-an-ivr" rel="noopener noreferrer"&gt;Build an IVR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/concepts/deployment" rel="noopener noreferrer"&gt;Deploy a Function&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published on the &lt;a href="https://sinch.com/blog/how-i-built-voice-ivr-sinch-functions/" rel="noopener noreferrer"&gt;Sinch blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sinch</category>
      <category>typescript</category>
      <category>serverless</category>
      <category>voice</category>
    </item>
    <item>
      <title>Yells at Cloud, Episode 3: Super Intelligence, Artificial Slowdown</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Mon, 28 Sep 2026 17:29:34 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/yells-at-cloud-episode-3-super-intelligence-artificial-slowdown-3o1b</link>
      <guid>https://dev.to/gunnargrosch/yells-at-cloud-episode-3-super-intelligence-artificial-slowdown-3o1b</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ws83xEExg_Y" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Listen: &lt;a href="https://www.youtube.com/watch?v=ws83xEExg_Y" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; · &lt;a href="https://open.spotify.com/episode/3QmF1PSE7tVhUDcAUAmmSX" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://podcasts.apple.com/us/podcast/super-intelligence-artificial-slowdown/id6789969050?i=1000792058753" rel="noopener noreferrer"&gt;Apple Podcasts&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The AI labs say they want to pace the frontier. About ten days later, they shipped new models and cut prices. Is "slow down" a safety commitment, marketing, or a way to write the rules before regulators do? Chris explains the EU Cyber Resilience Act and its new 24-hour clock for reporting exploited vulnerabilities. Danielle looks at Pizza Bot, AWS' open-source inbox for background agents. And newcomer Ken Collins joins to talk about building AI-native teams when anyone can vibe code something that looks finished.&lt;/p&gt;

&lt;p&gt;Burst Mode: Aurora DSQL finally gets foreign key constraints, Jev and Laya make decisions instead of generating text, an S3 bucket from Obama's inauguration day that nobody wants to delete, and AWS confirming permanent customer data loss in Bahrain and the UAE.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestamps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Main topics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0:00&lt;/code&gt;: Cold open&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3:57&lt;/code&gt;: Pacing the frontier: is "slow down" just marketing? (Gunnar)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;15:00&lt;/code&gt;: The EU Cyber Resilience Act starts the clock (Chris)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;22:53&lt;/code&gt;: Pizza Bot and the harness explosion (Danielle)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;28:31&lt;/code&gt;: Building AI-native teams (Ken)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Burst Mode&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;38:58&lt;/code&gt;: Burst Mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;39:06&lt;/code&gt;: Aurora DSQL foreign key constraints (Danielle)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;45:07&lt;/code&gt;: Jev, Laya, and decision models (Ken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;51:13&lt;/code&gt;: Can AI clean up forgotten cloud resources? (Chris)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;55:58&lt;/code&gt;: AWS permanently loses customer data in the Middle East (Gunnar)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pacing the frontier&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cnbc.com/2026/09/14/sam-altman-ai-slowdown-anthropic-amodei-musk.html" rel="noopener noreferrer"&gt;https://www.cnbc.com/2026/09/14/sam-altman-ai-slowdown-anthropic-amodei-musk.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/DavidSacks/status/2098973625252708460" rel="noopener noreferrer"&gt;https://twitter.com/DavidSacks/status/2098973625252708460&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.politico.com/news/2026/09/18/anthropic-openai-spacexai-google-sued-over-calls-to-pace-ai-development-01085023" rel="noopener noreferrer"&gt;https://www.politico.com/news/2026/09/18/anthropic-openai-spacexai-google-sued-over-calls-to-pace-ai-development-01085023&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;EU Cyber Resilience Act&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/security/2026/09/11/eus-cyber-resilience-act-starts-the-24-hour-vulnerability-clock/529582" rel="noopener noreferrer"&gt;https://www.theregister.com/security/2026/09/11/eus-cyber-resilience-act-starts-the-24-hour-vulnerability-clock/529582&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digital-strategy.ec.europa.eu/en/policies/cra-reporting" rel="noopener noreferrer"&gt;https://digital-strategy.ec.europa.eu/en/policies/cra-reporting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openssf.org/cra-ebook/" rel="noopener noreferrer"&gt;https://openssf.org/cra-ebook/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pizza Bot&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/opensource/introducing-pizza-bot-an-open-source-inbox-for-ai-agents-that-work-in-the-background/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/opensource/introducing-pizza-bot-an-open-source-inbox-for-ai-agents-that-work-in-the-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pizza-bot-app/pizza-bot" rel="noopener noreferrer"&gt;https://github.com/pizza-bot-app/pizza-bot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Building AI-native teams&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/research/labor-market-impacts" rel="noopener noreferrer"&gt;https://www.anthropic.com/research/labor-market-impacts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/devops/2026/09/18/microsoft-agentically-ports-copilot-runtime-to-rust-for-120k/5297549" rel="noopener noreferrer"&gt;https://www.theregister.com/devops/2026/09/18/microsoft-agentically-ports-copilot-runtime-to-rust-for-120k/5297549&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Aurora DSQL foreign key constraints&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/08/aurora-dsql-foreign-key-constraints/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/08/aurora-dsql-foreign-key-constraints/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Jev and Laya&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://typesafe.ai" rel="noopener noreferrer"&gt;https://typesafe.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/convaiinnovations/laya" rel="noopener noreferrer"&gt;https://huggingface.co/convaiinnovations/laya&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=49765348" rel="noopener noreferrer"&gt;https://news.ycombinator.com/item?id=49765348&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS spend limits&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/accounts/latest/reference/create-spend-limit.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/accounts/latest/reference/create-spend-limit.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS Middle East data loss&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cnbc.com/2026/09/15/aws-cant-restore-service-to-bahrain-uae-6-months-after-iran-strikes.html" rel="noopener noreferrer"&gt;https://www.cnbc.com/2026/09/15/aws-cant-restore-service-to-bahrain-uae-6-months-after-iran-strikes.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reuters.com/world/middle-east/amazons-aws-is-unable-restore-access-bahrain-one-uae-cloud-data-zone-after-war-2026-09-15/" rel="noopener noreferrer"&gt;https://www.reuters.com/world/middle-east/amazons-aws-is-unable-restore-access-bahrain-one-uae-cloud-data-zone-after-war-2026-09-15/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wildberries Architected Framework&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.chrisfarris.com/post/wildberry/" rel="noopener noreferrer"&gt;https://www.chrisfarris.com/post/wildberry/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;fwd:cloudsec&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fwdcloudsec.org" rel="noopener noreferrer"&gt;https://fwdcloudsec.org&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hosts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gunnar Grosch: &lt;a href="https://gunnargrosch.com" rel="noopener noreferrer"&gt;https://gunnargrosch.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Danielle Heberling: &lt;a href="https://danielleheberling.xyz" rel="noopener noreferrer"&gt;https://danielleheberling.xyz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chris Farris: &lt;a href="https://chrisfarris.com" rel="noopener noreferrer"&gt;https://chrisfarris.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ken Collins: &lt;a href="https://metaskills.net" rel="noopener noreferrer"&gt;https://metaskills.net&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Follow the show
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://yellsatcloudpod.com" rel="noopener noreferrer"&gt;https://yellsatcloudpod.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;YouTube: &lt;a href="https://youtube.com/@yellsatcloudpod" rel="noopener noreferrer"&gt;https://youtube.com/@yellsatcloudpod&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bluesky: &lt;a href="https://bsky.app/profile/yellsatcloudpod.com" rel="noopener noreferrer"&gt;https://bsky.app/profile/yellsatcloudpod.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Twitter / X: &lt;a href="https://twitter.com/yellsatcloudpod" rel="noopener noreferrer"&gt;https://twitter.com/yellsatcloudpod&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>security</category>
      <category>podcast</category>
    </item>
    <item>
      <title>Connect AI Agents to Phone Calls with Sinch Voice API v2</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:37:34 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/connect-ai-agents-to-phone-calls-with-sinch-voice-api-v2-4o1p</link>
      <guid>https://dev.to/gunnargrosch/connect-ai-agents-to-phone-calls-with-sinch-voice-api-v2-4o1p</guid>
      <description>&lt;p&gt;Connecting an AI agent to a live phone call means bridging the phone network and the agent. That can involve SIP integration or a raw media path over a WebSocket, plus speech-to-text, text-to-speech, and managing conversational turns between the caller and the agent. That is a lot of telephony and media infrastructure to build and operate around an AI agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.sinch.com/docs/voice-2.0/overview" rel="noopener noreferrer"&gt;Sinch Voice API v2&lt;/a&gt; takes that work off you. Its Voice Relay destination connects a live call to your WebSocket endpoint, Sinch runs speech-to-text and text-to-speech, and your application exchanges plain text. Voice API v2 also adds Voice Streams, a raw bidirectional audio path for the cases where you do want the media yourself.&lt;/p&gt;

&lt;p&gt;It's in public preview, so features, limits, documentation, and behavior can still change and no SLA applies, but it's open for testing, evaluation, and early production traffic. I ran the whole path end to end against the &lt;a href="https://github.com/sinch/sinch-voice-tutorials" rel="noopener noreferrer"&gt;tutorial example&lt;/a&gt;: a LangChain agent answering a real phone call, interruptions included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Voice Relay matters
&lt;/h2&gt;

&lt;p&gt;An AI agent usually starts as a text application. It accepts a message, calls a model, and returns a text response. Voice Relay moves the audio boundary into the Sinch platform, so you keep the agent, its tools, prompts, and state, and you work with text rather than raw audio. Interruptions come with it, so callers can barge in while a response is playing.&lt;/p&gt;

&lt;p&gt;That makes Voice Relay a practical entry point for an AI receptionist, product demo bot, and internal helpdesk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Voice API v2 model
&lt;/h2&gt;

&lt;p&gt;Voice API v2 organizes an interaction around three resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;session&lt;/strong&gt; groups related calls and connections and remains active until its associated calls end.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;call&lt;/strong&gt; represents one participant's connection, such as a phone, SIP, or streaming leg.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;bridge&lt;/strong&gt; connects calls within a session when multiple participants need to communicate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SVAML, the Sinch Voice API Markup Language, describes what happens during the interaction. A &lt;code&gt;dial&lt;/code&gt; command can create a call leg. Nested events can define what happens when the call is answered, is busy, times out, or fails. A webhook can take over when the application needs to make a dynamic decision.&lt;/p&gt;

&lt;p&gt;This model matters for AI agents because the conversation and the call are related, but they aren't the same thing. The AI agent manages the conversation. SVAML and the Voice API manage the call flow. That separation lets you add a human transfer, another call leg, or a different media path without putting all of that logic inside the model prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  You'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Sinch account with Voice API v2 access, at the &lt;a href="https://dashboard.sinch.com" rel="noopener noreferrer"&gt;Sinch Build Dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A Sinch project ID, access key ID, and access key secret&lt;/li&gt;
&lt;li&gt;An activated Sinch virtual number&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://developers.sinch.com/docs/voice-2.0/api-reference/voice" rel="noopener noreferrer"&gt;Voice API v2 service ID&lt;/a&gt;, from Voice &amp;gt; Programmable Voice &amp;gt; Services in the dashboard&lt;/li&gt;
&lt;li&gt;Python 3.10 or newer&lt;/li&gt;
&lt;li&gt;An API key for OpenAI, Anthropic Claude, or Google Gemini&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ngrok&lt;/code&gt; with a registered authtoken, or another way to expose a local server over &lt;code&gt;wss://&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The walkthrough uses the &lt;a href="https://github.com/sinch/sinch-voice-tutorials" rel="noopener noreferrer"&gt;&lt;code&gt;sinch-voice-tutorials&lt;/code&gt;&lt;/a&gt; repository. Its &lt;code&gt;4.1-voice-relay&lt;/code&gt; tutorial is a Python WebSocket server backed by LangChain, which reads its configuration from environment variables and supports OpenAI, Anthropic Claude, and Google Gemini.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect an agent with Voice Relay
&lt;/h2&gt;

&lt;p&gt;Clone the tutorials repository and move into the Voice Relay example:&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/sinch/sinch-voice-tutorials.git
&lt;span class="nb"&gt;cd &lt;/span&gt;sinch-voice-tutorials/4.1-voice-relay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an environment and install the example's 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;The commands here are for macOS and Linux. On Windows, use &lt;code&gt;python&lt;/code&gt; in place of &lt;code&gt;python3&lt;/code&gt; and activate with &lt;code&gt;.venv\Scripts\activate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Copy the example configuration:&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 a provider and its API key in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;PROVIDER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;
&lt;span class="py"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_LLM_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository's &lt;code&gt;.env.example&lt;/code&gt; uses &lt;code&gt;openai&lt;/code&gt; as its default provider. The example also supports &lt;code&gt;claude&lt;/code&gt; for Anthropic Claude and &lt;code&gt;gemini&lt;/code&gt; for Google Gemini, and &lt;code&gt;requirements.txt&lt;/code&gt; installs the packages for all three, so switching provider means changing &lt;code&gt;PROVIDER&lt;/code&gt; and &lt;code&gt;API_KEY&lt;/code&gt;. You can also set &lt;code&gt;MODEL&lt;/code&gt;, &lt;code&gt;TEMPERATURE&lt;/code&gt;, &lt;code&gt;MAX_TOKENS&lt;/code&gt;, and &lt;code&gt;PORT&lt;/code&gt;, whose defaults show up in the startup log below, plus &lt;code&gt;GREETING&lt;/code&gt; to change the &lt;code&gt;Hello!&lt;/code&gt; the agent opens with.&lt;/p&gt;

&lt;p&gt;Start the local server:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The server reports the system prompt it loaded, then its provider and model settings, then the local address it is listening on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[*] Loaded system prompt (3424 chars)
[*] Provider: openai  Model: gpt-4o  Temp: 0.7  MaxTokens: 1024
[*] Agent Relay listening on ws://0.0.0.0:8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that output before you make a call. A missing provider key or a broken dependency shows up here rather than halfway through a conversation.&lt;/p&gt;

&lt;p&gt;The agent's behavior comes from &lt;code&gt;system_prompt.md&lt;/code&gt;. Edit that file to change the persona, domain, or instructions, then restart &lt;code&gt;server.py&lt;/code&gt; because the prompt is loaded at startup.&lt;/p&gt;

&lt;p&gt;Sinch connects inward to your relay server, so it needs a public address. Leave &lt;code&gt;server.py&lt;/code&gt; running in the first terminal and open a second one. If you don't already have ngrok, install it and register an authtoken from the &lt;a href="https://dashboard.ngrok.com/get-started/your-authtoken" rel="noopener noreferrer"&gt;Your Authtoken&lt;/a&gt; page in the ngrok dashboard. The free tier covers this walkthrough, and ngrok refuses to start a tunnel until a token is configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ngrok
ngrok config add-authtoken YOUR_NGROK_AUTHTOKEN
ngrok http 8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ngrok takes over the terminal and prints a status table. The line that matters is the forwarding address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forwarding    https://YOUR_NGROK_ID.ngrok-free.dev -&amp;gt; http://localhost:8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take that address, change only the scheme to &lt;code&gt;wss://&lt;/code&gt;, and use it as the Voice Relay endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok address:  https://YOUR_NGROK_ID.ngrok-free.dev
Sinch endpoint: wss://YOUR_NGROK_ID.ngrok-free.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;wss://&lt;/code&gt; value goes in the Sinch service configuration, not in the relay server, which stays on local port &lt;code&gt;8765&lt;/code&gt;. Leave both &lt;code&gt;server.py&lt;/code&gt; and ngrok running from here on.&lt;/p&gt;

&lt;p&gt;The minimal Voice Relay destination looks like this:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VOICE_RELAY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"voiceRelay"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wss://YOUR_NGROK_ID.ngrok-free.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ttsVoice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tiffany"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sttLanguage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&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;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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;wss://&lt;/code&gt; address Sinch connects to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ttsVoice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Tiffany&lt;/code&gt; matches this tutorial. See the &lt;a href="https://developers.sinch.com/docs/voice-2.0/api-reference/text-to-speech-voices" rel="noopener noreferrer"&gt;supported voices reference&lt;/a&gt; before changing it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sttLanguage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;A BCP-47 language tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enableInterruptions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Defaults to &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;callHeaders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Up to 16 key/value pairs, each key and value 255 characters or fewer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your service needs the number before any of this matters. In the dashboard, open the service, go to Voice channels, and choose Configure on the Phone row, then Add numbers and pick your virtual number. A number belongs to one service at a time, so if it already sits with another service the dashboard asks you to confirm the reassignment, and incoming calls to that number follow the new service from then on.&lt;/p&gt;

&lt;p&gt;With the number in place, route the call by giving the service a static call behavior or a webhook that returns SVAML. The static configuration does five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Answer the incoming call.&lt;/li&gt;
&lt;li&gt;Add the inbound leg to &lt;code&gt;main-bridge&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a call leg with a &lt;code&gt;VOICE_RELAY&lt;/code&gt; destination.&lt;/li&gt;
&lt;li&gt;Add the relay leg to the same &lt;code&gt;main-bridge&lt;/code&gt; when it answers.&lt;/li&gt;
&lt;li&gt;Hang up the relay leg when the inbound leg ends.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;server.py&lt;/code&gt; and ngrok are both holding their terminals now, so open a third one for this step. Fill in the five values at the top of the request and run it. If you'd rather use the dashboard, paste the inner SVAML body from &lt;code&gt;static.txt&lt;/code&gt; into its predefined call behavior editor instead.&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;export &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT_ID
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_ACCESS_KEY_ID
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERVICE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_SERVICE_ID
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VOICE_RELAY_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wss://YOUR_NGROK_ID.ngrok-free.dev

&lt;span class="c"&gt;# prompts without echoing, so the secret stays out of shell history&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Access key secret: '&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-rs&lt;/span&gt; KEY_SECRET
&lt;span class="nb"&gt;echo
export &lt;/span&gt;KEY_SECRET

curl &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_ID&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$KEY_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://voice.api.sinch.com/v2/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &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="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;
{
  "callBehavior": {
    "type": "STATIC",
    "static": {
      "callName": "caller",
      "commands": [
        { "command": "answer" },
        { "command": "bridgeCall", "bridgeName": "main-bridge" },
        {
          "command": "dial",
          "callName": "voice_relay_call",
          "to": {
            "type": "VOICE_RELAY",
            "voiceRelay": {
              "endpoint": "&lt;/span&gt;&lt;span class="nv"&gt;$VOICE_RELAY_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;",
              "ttsVoice": "Tiffany",
              "sttLanguage": "en-US"
            }
          },
          "events": {
            "onAnswer": [
              { "command": "bridgeCall", "bridgeName": "main-bridge" }
            ]
          }
        }
      ],
      "events": {
        "onHangup": [
          { "command": "hangup", "callName": "voice_relay_call" }
        ]
      }
    }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;JSON
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful &lt;code&gt;PATCH&lt;/code&gt; returns the updated service, so you can read your endpoint back out of the response and confirm it took:&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;"serviceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SERVICE_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"projectId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"voice-relay-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isDefault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callBehavior"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STATIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"static"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"callName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"caller"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bridgeCall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bridgeName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main-bridge"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VOICE_RELAY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"voiceRelay"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wss://YOUR_NGROK_ID.ngrok-free.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"ttsVoice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tiffany"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"sttLanguage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"onAnswer"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bridgeCall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bridgeName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main-bridge"&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;span class="p"&gt;]&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;span class="nl"&gt;"callName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"voice_relay_call"&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;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"onHangup"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hangup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"callName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"voice_relay_call"&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;span class="p"&gt;]&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;span class="p"&gt;}&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;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;The inner SVAML body is also available in &lt;a href="https://github.com/sinch/sinch-voice-tutorials/blob/main/4.1-voice-relay/static.txt" rel="noopener noreferrer"&gt;&lt;code&gt;static.txt&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What success looks like
&lt;/h3&gt;

&lt;p&gt;Call the Sinch number associated with the service. You should hear &lt;code&gt;Hello!&lt;/code&gt;, or the greeting you set with &lt;code&gt;GREETING&lt;/code&gt;. Then speak a question and listen for the agent's response.&lt;/p&gt;

&lt;p&gt;The server terminal logs both directions of the WebSocket, and that log is the most useful thing on screen during a first call. Mine looked like this, trimmed to one exchange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[+] Connected: ('127.0.0.1', 65438)
  WS &amp;lt;&amp;lt; {"callHeaders":{},"callId":"01M25YACD3G61GH4Y0NQNWR4WD","interruptionsEnabled":true,...}
    connect  callId=01M25YACD3G61GH4Y0NQNWR4WD  serviceId=YOUR_SERVICE_ID
  WS &amp;gt;&amp;gt; {"command": "answer"}
  WS &amp;gt;&amp;gt; {"command": "text", "text": "Hello!", "isLast": true, "isInterruptible": true}
  WS &amp;lt;&amp;lt; {"batchSequence":0,"command":"textPlaybackStart"}
  WS &amp;lt;&amp;lt; {"batchSequence":0,"command":"textPlaybackStop"}
  WS &amp;lt;&amp;lt; {"reason":"speech-detected","command":"interrupt"}
  WS &amp;lt;&amp;lt; {"sttLanguage":"en-US","text":"Hi.","isCorrection":false,"command":"text"}
    STT → 'Hi.'
    LLM ← 'Hey there! How can I assist you today? If it involves voice tech or a good dad j'…
  WS &amp;gt;&amp;gt; {"command": "text", "text": "Hey there! How can I assist you today?...", "isLast": true}
  WS &amp;lt;&amp;lt; {"batchSequence":1,"command":"textPlaybackStart"}
  WS &amp;lt;&amp;lt; {"batchSequence":1,"command":"textPlaybackStop"}
[-] Connection closed (1006):
[-] Session ended  callId=01M25YACD3G61GH4Y0NQNWR4WD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in that log are worth knowing before your first call. The &lt;code&gt;connect&lt;/code&gt; message carries &lt;code&gt;interruptionsEnabled: true&lt;/code&gt;, which is where you can confirm the interruption default rather than taking it from the configuration reference. And the connection closes with code &lt;code&gt;1006&lt;/code&gt; when the caller hangs up, which reads as an abnormal closure but is what a normal hangup looks like here.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;interrupt&lt;/code&gt; with &lt;code&gt;reason=speech-detected&lt;/code&gt; appears on every turn, because Sinch sends one whenever it hears the caller. Its position is what tells you something: after &lt;code&gt;textPlaybackStop&lt;/code&gt; it just marks the start of the caller's turn, while between &lt;code&gt;textPlaybackStart&lt;/code&gt; and &lt;code&gt;textPlaybackStop&lt;/code&gt; it is a barge-in. When I talked over a response, playback stopped and the next transcript was what I had said over the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  WS &amp;lt;&amp;lt; {"batchSequence":5,"command":"textPlaybackStart"}
  WS &amp;lt;&amp;lt; {"reason":"speech-detected","command":"interrupt"}
  WS &amp;lt;&amp;lt; {"batchSequence":5,"command":"textPlaybackStop"}
  WS &amp;lt;&amp;lt; {"sttLanguage":"en-US","text":"I'm going to stop you right there.",...,"command":"text"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a voice interface in front of a text-based AI agent, without handling a single audio frame yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where your application sits
&lt;/h2&gt;

&lt;p&gt;Sinch and the WebSocket server exchange a small JSON protocol, one command per message, and &lt;code&gt;server.py&lt;/code&gt; handles it for you. Transcript corrections are the part worth understanding before you build on the example.&lt;/p&gt;

&lt;p&gt;Sinch sends an early transcript, then may send a corrected one for the same utterance with &lt;code&gt;isCorrection: true&lt;/code&gt;, and the corrected text includes the earlier text rather than replacing only the changed part. It happened once in three calls: &lt;code&gt;Thank you.&lt;/code&gt; was followed by &lt;code&gt;Thank you.\nOh, that's nice.&lt;/code&gt;, and both went to the model, so the agent answered the same utterance twice. Decide whether you wait for a correction, cancel the in-flight request, or ignore corrections entirely.&lt;/p&gt;

&lt;p&gt;The application boundary 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;Caller speech
    |
Sinch speech-to-text
    |
Voice Relay text message
    |
AI agent
    |
Voice Relay text response
    |
Sinch text-to-speech
    |
Caller hears the response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Voice Relay or raw audio streaming?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choose&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voice Relay&lt;/td&gt;
&lt;td&gt;Your agent accepts text and returns text, and Sinch handles STT and TTS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice Streams&lt;/td&gt;
&lt;td&gt;You need raw audio and own the STT/TTS pipeline.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Voice Streams suits a custom speech pipeline, a specialized audio processor, or a provider whose protocol requires direct audio access. It also means you own more of the latency and failure behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production considerations
&lt;/h2&gt;

&lt;p&gt;Voice interactions make latency visible. A slow model response creates silence for the caller, so measure the time from the incoming speech event to the first response and to the completed response. Use a model and prompt that fit a phone conversation rather than optimizing only for maximum answer quality.&lt;/p&gt;

&lt;p&gt;The repository example waits for the full model response before sending anything back, so the caller hears nothing until the model has finished generating. That is the first place to look if the pauses feel long.&lt;/p&gt;

&lt;p&gt;The example also keeps conversation history in memory for each WebSocket connection. That's fine for a first test, but it isn't a durable conversation store. Decide what state belongs to a call, what belongs to a customer, and what has to survive a reconnect.&lt;/p&gt;

&lt;p&gt;Interruptions need explicit handling. If the caller speaks while a response is playing, you can receive an interruption event while an LLM request is still running. Cancel the request where you can, or tag each response with a turn ID and discard stale output before sending it back to Sinch. The same turn ID handles corrected transcripts, which arrive as a second &lt;code&gt;text&lt;/code&gt; message for an utterance you have already sent to the model.&lt;/p&gt;

&lt;p&gt;The local example also needs stronger failure handling before production use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send a short fallback response when the model provider fails.&lt;/li&gt;
&lt;li&gt;Persist conversation state when the call needs to survive a process restart.&lt;/li&gt;
&lt;li&gt;Keep the WebSocket endpoint available for the full call lifetime.&lt;/li&gt;
&lt;li&gt;Log call, session, connection, and model timing identifiers without logging sensitive conversation content unnecessarily.&lt;/li&gt;
&lt;li&gt;Use OAuth 2.0 client credentials for production API access. Basic authentication is useful for initial testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;Check the provider configuration before you make a call. A bad or missing LLM key shows up as a silent call rather than an error, because the model isn't called until the first turn, so confirm that &lt;code&gt;PROVIDER&lt;/code&gt; and &lt;code&gt;API_KEY&lt;/code&gt; match each other in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The free ngrok URL changes when the tunnel restarts. Update the &lt;code&gt;endpoint&lt;/code&gt; in the Voice API v2 service configuration whenever that happens, or Sinch can't reach the current server. &lt;code&gt;server.py&lt;/code&gt; doesn't need restarting for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview status
&lt;/h2&gt;

&lt;p&gt;The preview terms allow testing, evaluation, early commercial use, and live traffic, and state that the platform is designed to support production-level volumes. The service is provided as is and as available, usage limits may apply, and no SLA applies. Sinch may add functionality, and breaking changes cannot be ruled out before general availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Voice Relay is one path into Voice API v2. The same platform model also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Outbound voice alerts with text-to-speech&lt;/li&gt;
&lt;li&gt;Answering machine detection&lt;/li&gt;
&lt;li&gt;Batch calling with call pacing&lt;/li&gt;
&lt;li&gt;Call recording and transcription&lt;/li&gt;
&lt;li&gt;Number masking&lt;/li&gt;
&lt;li&gt;SIP connections&lt;/li&gt;
&lt;li&gt;Raw audio streaming&lt;/li&gt;
&lt;li&gt;Live call control through SVAML and webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://github.com/sinch/sinch-voice-tutorials" rel="noopener noreferrer"&gt;Sinch Voice API v2 tutorials repository&lt;/a&gt; has working examples for these paths. Start with the &lt;code&gt;4.1-voice-relay&lt;/code&gt; tutorial for a text-based agent, then move to &lt;code&gt;4.2-stream-audio&lt;/code&gt; when you need direct access to the audio stream.&lt;/p&gt;

&lt;p&gt;Voice API v2 gives an existing text agent a route into a live voice interaction without turning that agent into an audio-processing system. Clone &lt;code&gt;4.1-voice-relay&lt;/code&gt;, point &lt;code&gt;system_prompt.md&lt;/code&gt; at your own agent's prompt, and call the number.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/voice-2.0/overview" rel="noopener noreferrer"&gt;Sinch Voice API v2 overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/voice-2.0/getting-started" rel="noopener noreferrer"&gt;Voice API v2 getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/voice-2.0/api-reference/voice" rel="noopener noreferrer"&gt;Voice API v2 API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/voice-2.0/api-reference/text-to-speech-voices" rel="noopener noreferrer"&gt;Text-to-Speech voices reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sinch/sinch-voice-tutorials" rel="noopener noreferrer"&gt;Voice API v2 tutorials repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would you connect to a voice agent first: customer support, an internal helpdesk, or a product demo? Share the use case you would test on Voice API v2.&lt;/p&gt;

&lt;p&gt;Originally published on the &lt;a href="https://sinch.com/blog/ai-agent-phone-calls-voice-api-relay/" rel="noopener noreferrer"&gt;Sinch blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sinch</category>
      <category>ai</category>
      <category>voice</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Bivack: A Cloud Dev Sandbox for Coding Agents on AWS Lambda MicroVMs</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:45:25 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/building-bivack-a-cloud-dev-sandbox-for-coding-agents-on-aws-lambda-microvms-24o6</link>
      <guid>https://dev.to/gunnargrosch/building-bivack-a-cloud-dev-sandbox-for-coding-agents-on-aws-lambda-microvms-24o6</guid>
      <description>&lt;p&gt;I released a new version of &lt;a href="https://github.com/gunnargrosch/bivack" rel="noopener noreferrer"&gt;Bivack&lt;/a&gt; this morning, and I built it from a car, a bus, an airport and a flight to Amsterdam. Laptop when there was a table, phone when there was not. The agent doing the work never moved: it sat on a MicroVM in &lt;code&gt;us-east-1&lt;/code&gt; the whole time, with my home directory in Amazon S3 where I had left it the night before.&lt;/p&gt;

&lt;p&gt;That is the whole premise. Most of what I have written about agentic coding assumes the agent runs on your machine. Claude Code with Amazon Bedrock, hooks, MCP servers, plugins: all of it assumes a terminal on your laptop and a project on your local disk. That setup is excellent until you want to close the laptop, hand a task to an agent and walk away, or pick the work back up from a phone in a departure hall.&lt;/p&gt;

&lt;p&gt;Bivack gives each person their own AWS Lambda MicroVM, with their coding agents on it and a persistent home on Amazon S3 Files, reached from a browser: a terminal or a full VS Code workbench. One machine per user, not one per agent, so the agents you selected all sit on the same box and share the same files. The name is a nod to a bivouac, a shelter you set up where you need it. One limit up front: the home is network storage, so it is slower than a local disk. This post covers how it works, the decisions that shaped it, and the parts that are harder than they look.&lt;/p&gt;

&lt;p&gt;On the trip, I started work in the browser on my phone, then picked up the exact same session on my laptop when I had a table. The terminal made the phone leg workable; on the laptop, the workbench let me inspect the files and changes the agent had made rather than reconnecting only to a shell. I moved back and forth between the two several times before I shipped the release. There was no handoff to manage or context to recreate: the agent, the terminal and the files stayed put on the MicroVM while I changed devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the agent needs its own machine
&lt;/h2&gt;

&lt;p&gt;The friction with a laptop-bound agent is mostly the laptop. The session is tied to that one machine, the provider logins live on it, and a long task means keeping the terminal open and the machine awake. You also cannot hand the same environment to a teammate without them repeating your setup.&lt;/p&gt;

&lt;p&gt;What I wanted was narrow and specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One disposable machine per person, isolated from everyone else.&lt;/li&gt;
&lt;li&gt;A home directory that survives restarts, so files and logins are still there next week.&lt;/li&gt;
&lt;li&gt;Reachable from a browser, including from a phone or an iPad, without installing anything.&lt;/li&gt;
&lt;li&gt;Each person brings their own provider login, rather than a shared key in the image.&lt;/li&gt;
&lt;li&gt;A real editor, not just a terminal. I want to read and change what the agent wrote.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is why the workbench exists. Handing a task to an agent and reading a summary of what it did is not how I work. I want to open the diff, disagree with a function name, move a file, run the tests myself and then hand it back. A terminal covers the agent side of that fine, and it is thin for the rest: reviewing a multi-file change through a CLI pager on a phone is not reviewing. So the editor is not a nicety bolted onto the terminal, it is half the point, and both halves had to be the same machine for it to mean anything.&lt;/p&gt;

&lt;p&gt;AWS Lambda MicroVMs matched those requirements closely. A Firecracker snapshot boots in a fraction of a second, each MicroVM is isolated, and you are billed while one is running. For an interactive sandbox that should feel instant and cost nothing while idle, that is the right primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What using it is like
&lt;/h2&gt;

&lt;p&gt;Sign in, pick a workspace, and you are in. The terminal and the editor are the same machine, so they share one home, one set of files and one set of logins. Because it is one machine, you can run an agent in a terminal pane and a second one beside it, both working the same repo. Bringing work in is the usual &lt;code&gt;git clone&lt;/code&gt;; git and &lt;code&gt;gh&lt;/code&gt; are already installed.&lt;/p&gt;

&lt;p&gt;The numbers behind the walk-away part: a VM suspends after two hours with no inbound traffic, stays resumable for another thirty minutes, and no single VM lives longer than eight hours. So you have roughly two and a half hours of not touching it before the session is gone, and the running agent process goes with it. The home survives either way, which is the part that matters: files, shell history and every login are still there on the next sign-in. All three timers are &lt;code&gt;deploy.env&lt;/code&gt; settings (&lt;code&gt;IDLE_MAX_SECONDS&lt;/code&gt;, &lt;code&gt;IDLE_SUSPEND_SECONDS&lt;/code&gt;, &lt;code&gt;MAX_LIFETIME_SECONDS&lt;/code&gt;), so a longer leash costs awake time and nothing else. A travel day fits inside the defaults, which is how I picked them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The chooser
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9iknax6nji36ckh2in2k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9iknax6nji36ckh2in2k.png" alt="The Bivack chooser" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entry point: two buttons, one for the terminal and one for the editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  The terminal
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih6x5kr7v6epd8byuisy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih6x5kr7v6epd8byuisy.png" alt="The Bivack terminal" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An xterm.js terminal that installs as a PWA and adds a touch key row on devices without a keyboard. It is where the agent CLIs run, each signed in with that person's own account. This is the half I used from a phone, and the touch key row is the reason it was usable at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The workbench
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuw6ybekqsx35l2haxo5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuw6ybekqsx35l2haxo5h.png" alt="The Bivack VS Code workbench" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workbench is the real browser build of VS Code, backed by the same home and the same authenticated WebSocket mechanism. Same files, same logins, same machine as the terminal, so an agent can be working in one tab while I read its output in the other. Click a file path in its terminal and the file opens in the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not a hosted agent or a VPS
&lt;/h2&gt;

&lt;p&gt;Two alternatives are worth naming. The hosted agents (Claude Code and Codex in the cloud, plus background agents from Cursor, Copilot and Devin) are less work: point one at a repo and it runs on the vendor's infrastructure with the vendor's model, usually per seat, with your code in their accounts. What you give up is the environment. A small VPS running &lt;code&gt;tmux&lt;/code&gt; and &lt;code&gt;code-server&lt;/code&gt; is the other shape, and it costs about five dollars a month, but everyone shares one host and one kernel, you keep the box running, and the home dies with the box. Isolation, fast starts and a home that outlives the machine are exactly what Lambda MicroVMs and S3 Files are for.&lt;/p&gt;

&lt;p&gt;The starting point was &lt;a href="https://github.com/singledigit/microvm-dev-environment" rel="noopener noreferrer"&gt;Remote Developer (rDev)&lt;/a&gt; by &lt;a href="https://github.com/singledigit" rel="noopener noreferrer"&gt;Eric Johnson&lt;/a&gt;, which proved the core idea: per-user AWS Lambda MicroVMs behind Cognito, a browser terminal over a WebSocket, and a per-user S3 Files home mounted by a lifecycle hook. Bivack keeps that foundation and takes it somewhere else. rDev's sandbox carried credentials for Amazon Bedrock and the AWS account hosting it; Bivack removes that hosting-account access. The browser VS Code workbench is new, as is the chooser and login frontend around it, the configurable MicroVM image, the one-command deploy and teardown, the NAT modes, the budget wiring, and the break-glass tooling. The attribution is in &lt;a href="https://github.com/gunnargrosch/bivack/blob/main/NOTICE" rel="noopener noreferrer"&gt;NOTICE&lt;/a&gt;, and it is genuine: the template shape, the image bones, the lifecycle hooks and the WebSocket terminal all trace back to Eric's work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shapes everything
&lt;/h2&gt;

&lt;p&gt;One constraint drives most of the design, so the request path is the place to start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
  |-- HTTPS ------&amp;gt; CloudFront (static frontend)
  |-- sign in ----&amp;gt; Cognito
  |-- GET /token -&amp;gt; API Gateway -&amp;gt; token Lambda function
  |                                   |-- find or create this user's access point
  |                                   '-- launch or resume the MicroVM
  |                                       -&amp;gt; { authToken, endpoint }
  '-- wss:// -----&amp;gt; MicroVM agent (file system, PTY)
                         '-- mount /home/coder (the /run lifecycle hook)
                                 -&amp;gt; S3 Files access point for this user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cognito holds the users, and they are admin-created: adding a teammate means creating their login by hand, and each one gets their own access point and MicroVM. API Gateway validates the JWT before the Lambda function runs, so the function only ever sees a verified &lt;code&gt;sub&lt;/code&gt;. That &lt;code&gt;sub&lt;/code&gt; maps to one S3 Files access point rooted at &lt;code&gt;/users/&amp;lt;sub&amp;gt;&lt;/code&gt; and one MicroVM. On the first request it creates the access point, launches a MicroVM, and returns a short-lived token and the endpoint. The MicroVM mounts its own home during the &lt;code&gt;/run&lt;/code&gt; lifecycle hook, so the mount is per-user and the image stays shared.&lt;/p&gt;

&lt;p&gt;Here is the constraint. A Lambda MicroVM endpoint accepts the auth token only in the &lt;code&gt;X-aws-proxy-auth&lt;/code&gt; header. A browser can set that header on a &lt;code&gt;fetch&lt;/code&gt; or a WebSocket handshake, but it cannot set a header on a page navigation, or on any of the subresource loads a page makes after it. So you cannot run a web IDE inside the MicroVM and point the browser at it. Not with a redirect, not with a cookie, not with a query string.&lt;/p&gt;

&lt;p&gt;That single fact decides the shape of the entire frontend. The terminal is an xterm.js client rather than a served page. The IDE is a self-hosted workbench that talks to the VM over a WebSocket rather than an editor served from the VM. Both run over &lt;code&gt;wss://&lt;/code&gt;, and the endpoint is useless without the short-lived token. Every design decision downstream of the ingress follows from a header a browser is not allowed to set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in the image
&lt;/h2&gt;

&lt;p&gt;The sandbox is not a machine you install things on. It is an image you declare, and &lt;code&gt;deploy.env&lt;/code&gt; is where you declare it:&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;CLAUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;latest           &lt;span class="c"&gt;# install the current release&lt;/span&gt;
&lt;span class="c"&gt;# OPENCODE=latest       # commented out disables it&lt;/span&gt;
&lt;span class="nv"&gt;KIRO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;                   &lt;span class="c"&gt;# empty also disables it&lt;/span&gt;
&lt;span class="nv"&gt;TOFU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.12.6             &lt;span class="c"&gt;# install this pinned release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four coding agents are available (&lt;code&gt;CLAUDE&lt;/code&gt;, &lt;code&gt;CODEX&lt;/code&gt;, &lt;code&gt;OPENCODE&lt;/code&gt;, &lt;code&gt;KIRO&lt;/code&gt;) and four infrastructure CLIs (&lt;code&gt;CDK&lt;/code&gt;, &lt;code&gt;SAM&lt;/code&gt;, &lt;code&gt;TOFU&lt;/code&gt;, &lt;code&gt;TERRAFORM&lt;/code&gt;). Claude Code is the only one enabled in the generated &lt;code&gt;deploy.env&lt;/code&gt;; the rest are opt-in, because every tool is weight in an image that every user boots. Setting a tool to &lt;code&gt;latest&lt;/code&gt; takes its current release and a version string pins it. Kiro CLI is &lt;code&gt;latest&lt;/code&gt; only, and pinning OpenTofu and Terraform is worth doing if you want the same image twice.&lt;/p&gt;

&lt;p&gt;Changing any of those settings changes the packaged MicroVM source hash, so the next &lt;code&gt;./scripts/deploy.sh&lt;/code&gt; rebuilds the image on its own. The trade-off is worth stating plainly: adding a tool is a five to ten minute image build and a VM recycle, not &lt;code&gt;npm install -g&lt;/code&gt;. In exchange, nobody hand-installs anything into a sandbox that gets thrown away, and every user gets the same machine.&lt;/p&gt;

&lt;p&gt;Each selected CLI signs in once and keeps its session under &lt;code&gt;/home/coder&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CLI&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;First-run login&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sign in with your Claude plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sign in with your OpenAI account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opencode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/connect&lt;/code&gt; to add a provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kiro CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kiro-cli&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kiro-cli login&lt;/code&gt; (device flow)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They share workspace files while keeping their own configuration and history, and each is configured for unattended work: Claude Code uses &lt;code&gt;bypassPermissions&lt;/code&gt;, Codex bypasses approvals and its local sandbox, and Kiro CLI carries a persistent allow-all policy. The MicroVM is the isolation boundary, which is the only reason that is a reasonable default.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things I learned building this
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Put the home on S3 Files, not a disk
&lt;/h3&gt;

&lt;p&gt;The home has to outlive the machine. MicroVMs suspend when idle and terminate after a maximum lifetime, so anything stored only on the VM's local disk is gone when it recycles. I considered EBS, EFS and S3 Files. EBS is tied to one instance and one Availability Zone, which fights the recycle-and-come-back model. EFS would work, and its access points can root at a directory with an enforced UID and GID, so isolation is not the hard part. What made S3 Files the better fit is that the home is the same bytes as an S3 prefix: an access point rooted at &lt;code&gt;/users/&amp;lt;sub&amp;gt;&lt;/code&gt; is all that user can see, and everything they write is also readable as ordinary objects in the bucket with the tools I already have.&lt;/p&gt;

&lt;p&gt;The trade-off: file operations go over NFS to a network-backed filesystem, so metadata-heavy work is slower than local disk. You notice it when a CLI scans its config or a large cache. It is the right call for durability, and it is not free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioning comes with S3 Files, and so does the bill
&lt;/h3&gt;

&lt;p&gt;S3 Files requires versioning on the bucket (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html" rel="noopener noreferrer"&gt;prerequisites&lt;/a&gt;). That is fine until you realize how much a home directory churns: package caches, session history, plugin state, logs. Every write becomes a new object version and every delete becomes a delete marker, so the bucket grows even when it looks empty. A lifecycle rule that expires noncurrent versions after a week, drops expired delete markers and aborts stalled multipart uploads keeps the growth bounded. Without it you are paying to store every intermediate state of a file nobody will look at again.&lt;/p&gt;

&lt;h3&gt;
  
  
  The web workbench is its own project
&lt;/h3&gt;

&lt;p&gt;I expected the IDE to be configuration on top of an existing package. It is closer to building an application. The &lt;code&gt;FileSystemProvider&lt;/code&gt; and the terminal backend both run over the same authenticated WebSocket to a small agent in the VM. There is no local file system to read, and the ingress constraint rules out serving the editor from the VM, so the workbench stays a client-side app that talks to the agent. The provider turns that socket into &lt;code&gt;stat&lt;/code&gt;, &lt;code&gt;readDirectory&lt;/code&gt;, &lt;code&gt;readFile&lt;/code&gt;, &lt;code&gt;writeFile&lt;/code&gt;, &lt;code&gt;createDirectory&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;rename&lt;/code&gt; and a recursive &lt;code&gt;watch&lt;/code&gt;, and the agent pushes change events back so the workbench sees an external edit. Making VS Code treat a remote-over-socket tree as a normal workspace, with all of that, takes more wiring than the documentation suggests.&lt;/p&gt;

&lt;p&gt;One thing that surprised me: a browser workbench keeps its user settings in the browser, in IndexedDB, not on the machine it is editing. Workspace settings (&lt;code&gt;.vscode/settings.json&lt;/code&gt;) live in the VM, but the global ones do not follow you between browsers. And if you set defaults by writing the user settings file, you only ever apply them once, because the file already exists on the next load. Registering configuration defaults instead lets people override the ones they care about and have it stick.&lt;/p&gt;

&lt;h3&gt;
  
  
  No AWS credentials in the sandbox, on purpose
&lt;/h3&gt;

&lt;p&gt;Two sets of AWS accounts are in play here, and they are separate. One hosts the stack: the MicroVMs, the bucket, the API. The others are whatever accounts you use for your own work. The sandbox gets no credentials for the hosting account. Its execution role can mount S3 Files and look up its own endpoint, and nothing more. When you need AWS inside the VM, you bring your own account in.&lt;/p&gt;

&lt;p&gt;The profiles and token cache live in &lt;code&gt;~/.aws&lt;/code&gt; in the home, so you set them up once rather than once per VM.&lt;/p&gt;

&lt;p&gt;rDev took a different path: its MicroVM execution role gives the sandbox Amazon Bedrock access and broad, boundary-limited permissions in the AWS account that runs the stack. You can still add your own provider login there. I moved away from giving the sandbox hosting-account credentials at all: that avoids coupling it to one provider and keeps a compromised agent out of the account that runs everything.&lt;/p&gt;

&lt;p&gt;The reach of a compromised agent is the sandbox and everything signed into it. Provider logins persist in the home, so a malicious file that the agent reads can reach &lt;code&gt;~/.claude&lt;/code&gt;, &lt;code&gt;~/.codex&lt;/code&gt;, &lt;code&gt;~/.kiro&lt;/code&gt;, the &lt;code&gt;gh&lt;/code&gt; config, an &lt;code&gt;aws sso&lt;/code&gt; cache and git credentials. The agent cannot touch the AWS account the stack runs in, which is the line I care about, and it can touch every other account the person signed into inside the sandbox. That is the boundary.&lt;/p&gt;

&lt;p&gt;On a shared deployment there is one more reader to account for: the home lives in the bucket owned by whoever operates the stack, so the operator can read every teammate's provider logins. On a personal deployment that is the same person. On a company deployment, it is a decision to make on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egress has two prices
&lt;/h3&gt;

&lt;p&gt;Each MicroVM reaches model providers and package registries through the private subnets, so those subnets need a way out to the internet. The default is a small NAT instance running masquerade: a &lt;code&gt;t4g.nano&lt;/code&gt; at a few dollars a month, patched weekly by an SSM association. Set &lt;code&gt;NAT_MODE=gateway&lt;/code&gt; and it becomes a managed NAT Gateway at roughly ten times the monthly cost, with the patching handled for you. For a small team the instance is the sensible default; pick the gateway if you would rather not own the patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI v2, authenticated. 2.35.10 or newer, which is the release that added &lt;code&gt;aws lambda-microvms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;AWS SAM CLI 1.163.0 or newer, the first release whose transform expands &lt;code&gt;AWS::Serverless::MicrovmImage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Node.js 20 and npm&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zip&lt;/code&gt; and &lt;code&gt;python3&lt;/code&gt; (packaging and small helpers in the scripts)&lt;/li&gt;
&lt;li&gt;A region where AWS Lambda MicroVMs are available (the examples use &lt;code&gt;us-east-1&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker is not required. The MicroVM image is built server-side by the Lambda MicroVMs build service. The deploy checks the AWS CLI for MicroVM support before it applies a stack update, because an older CLI is the first thing most people hit.&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/gunnargrosch/bivack
&lt;span class="nb"&gt;cd &lt;/span&gt;bivack
&lt;span class="nb"&gt;cp &lt;/span&gt;deploy.env.example deploy.env
&lt;span class="nv"&gt;$EDITOR&lt;/span&gt; deploy.env
./scripts/deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy.env&lt;/code&gt; starts with three values:&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;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-profile
&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;span class="nv"&gt;LOGIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else is optional: the tool settings above, the stack name (&lt;code&gt;STACK_NAME&lt;/code&gt;), the memory tier (&lt;code&gt;MEMORY_MIB&lt;/code&gt;), the three timers, the first login's temporary password (&lt;code&gt;INITIAL_PASSWORD&lt;/code&gt;, random if unset), egress (&lt;code&gt;NAT_MODE&lt;/code&gt;), and the budget (&lt;code&gt;BUDGET_EMAIL&lt;/code&gt;, &lt;code&gt;BUDGET_USD&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The first run bootstraps the stack, builds the IDE, packages the MicroVM image, uploads the frontend, creates your first login, and smoke-tests a throwaway MicroVM. The image build alone is 5 to 10 minutes, so the first deploy is not a quick one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FrontendUrl: https://&amp;lt;id&amp;gt;.cloudfront.net
Login:       you@example.com

First sign-in: open the URL, sign in with the temporary password below, and
set a new password when prompted.

  Temporary password: &amp;lt;printed once&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;BUDGET_EMAIL&lt;/code&gt; and the stack also creates a monthly AWS Budget, $25 unless you change &lt;code&gt;BUDGET_USD&lt;/code&gt;, that emails that address at 80% and 100%. Leave it unset and no budget is created. It is worth setting: a sandbox left running shows up as an alert rather than a surprise. Cost tracks awake time and storage rather than seats, since the MicroVM bills only while it is running and a suspended VM costs nothing but the home's storage.&lt;/p&gt;

&lt;p&gt;Sign in, pick the terminal or the editor, and the CLIs sign in with your own accounts. The first launch of &lt;code&gt;claude&lt;/code&gt; on a brand-new VM does its first-run work, so it is slower than every launch after it. The image build runs a &lt;code&gt;/validate&lt;/code&gt; hook that exercises that startup path so the platform prefetches it, which brings the cold start back down.&lt;/p&gt;

&lt;p&gt;To use AWS from inside the VM, run &lt;code&gt;aws configure sso&lt;/code&gt; once to create a session and profile, then sign in by naming one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sso login &lt;span class="nt"&gt;--profile&lt;/span&gt; management &lt;span class="nt"&gt;--use-device-code&lt;/span&gt;
&lt;span class="c"&gt;# or log the whole session in&lt;/span&gt;
aws sso login &lt;span class="nt"&gt;--sso-session&lt;/span&gt; my-org &lt;span class="nt"&gt;--use-device-code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare &lt;code&gt;aws sso login&lt;/code&gt; resolves the &lt;code&gt;default&lt;/code&gt; profile, which usually has no SSO keys. &lt;code&gt;--use-device-code&lt;/code&gt; is required because the default Authorization Code flow redirects to the VM's own &lt;code&gt;127.0.0.1&lt;/code&gt;, which your browser cannot reach. The device-code flow instead prints a URL and code that work from any device.&lt;/p&gt;

&lt;p&gt;There is no SSH into a MicroVM. For break-glass access, &lt;code&gt;tools/exec.js&lt;/code&gt; opens a shell on a running VM and &lt;code&gt;tools/run-remote.js&lt;/code&gt; runs a single command against it. To remove everything the stack created, run &lt;code&gt;./scripts/teardown.sh&lt;/code&gt;: it deletes the stack, the buckets including their object versions, the Cognito user pool, the SSM parameters, running MicroVMs, the MicroVM images and the log groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull
./scripts/deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy.sh&lt;/code&gt; rebuilds only what changed: the IDE when its sources moved, the MicroVM image when &lt;code&gt;microvm/&lt;/code&gt; or a tool setting moved, the frontend when its assets or generated config moved. A no-change run skips frontend publishing, the CloudFront invalidation and the artifact upload. The buckets and the S3 Files home are never touched, so files, history and logins survive an upgrade. Running VMs keep the old image until they recycle; hit the terminal's power button to move onto a new one immediately.&lt;/p&gt;

&lt;p&gt;Two flags are worth knowing before a stack update. &lt;code&gt;--dry-run&lt;/code&gt; creates a CloudFormation changeset without executing it, and &lt;code&gt;--review&lt;/code&gt; prints the changeset and waits for you to confirm. What you are looking for there is &lt;code&gt;Replace&lt;/code&gt; on &lt;code&gt;UserPool&lt;/code&gt;, &lt;code&gt;WorkspaceBucket&lt;/code&gt; or &lt;code&gt;S3FilesFileSystem&lt;/code&gt;, because a replacement on any of those drops users or data. The one time you want it is bumping &lt;code&gt;S3FilesFileSystem&lt;/code&gt;'s &lt;code&gt;ClientToken&lt;/code&gt;, which is the recovery path for a wedged synchronization state. The rest of the flags are smaller: &lt;code&gt;--frontend-only&lt;/code&gt; re-uploads the frontend and IDE with no SAM deploy and no image, &lt;code&gt;--no-smoke&lt;/code&gt; skips the throwaway test VM, and &lt;code&gt;--build-ide&lt;/code&gt; forces an IDE rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;The IDE is the browser build of VS Code, so only extensions with a web entrypoint install, and Microsoft and GitHub sign-in and Settings Sync are unavailable because they are Microsoft-hosted built-ins. Source Control has no provider at all: the browser cannot spawn the git binary and monaco-vscode-api does not ship VS Code's git extension, so &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;gh&lt;/code&gt; in the terminal are the answer. IDE user settings are per-browser, and auto-save starts off with tooling dotfiles hidden behind &lt;code&gt;files.exclude&lt;/code&gt;, both overridable defaults.&lt;/p&gt;

&lt;p&gt;The home is network storage, so it is slower than a local disk. Inside the VM, &lt;code&gt;coder&lt;/code&gt; has passwordless &lt;code&gt;sudo&lt;/code&gt; and the workload has open outbound internet, so the MicroVM is the isolation boundary rather than a hardened jail. Model-provider spend is billed to each user's own provider account with no sandbox-level cap. The stack targets one region.&lt;/p&gt;

&lt;p&gt;There is no built-in preview for a dev server. The ingress accepts the auth token only in a header, so there is no port-forward from the browser to a port inside the VM; if you need one, run your own tunnel from inside the VM. The VM memory is a fixed tier (&lt;code&gt;MEMORY_MIB&lt;/code&gt;, 4 GB by default, bursting to 4x), and that tier is the ceiling an agent works within.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The travel day worked. I built and released a version of Bivack using the previous version of Bivack, from three modes of transport and two devices, and the only thing I actually needed was a browser and my own logins.&lt;/p&gt;

&lt;p&gt;What still stays on my laptop: anything that needs a dev server I can open in a browser tab, anything metadata-heavy enough that NFS latency turns into waiting, and anything I want to leave running for a full workday without touching it. The eight hour ceiling and the two and a half hour idle window are generous for a commute and thin for a weekend batch job.&lt;/p&gt;

&lt;p&gt;The parts that took the most work were the plumbing around the agent CLIs: a durable home on S3 Files, a workbench that had to become a real application, and a credentials story that stays deliberately empty. If you deploy it, the S3 versioning bill is the one to watch, and the first launch of any CLI on a fresh machine is the slow one.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/gunnargrosch/bivack" rel="noopener noreferrer"&gt;Bivack source&lt;/a&gt; and &lt;a href="https://github.com/gunnargrosch/bivack/releases" rel="noopener noreferrer"&gt;releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gunnargrosch/bivack/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;Bivack changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/singledigit/microvm-dev-environment" rel="noopener noreferrer"&gt;Remote Developer (rDev) by Eric Johnson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html" rel="noopener noreferrer"&gt;S3 Files prerequisites (versioning requirement)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-synchronization.html" rel="noopener noreferrer"&gt;Amazon S3 Files synchronization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would you run here, and what would you keep on your laptop? Let me know in the comments.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>productivity</category>
      <category>ide</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building a Communications Stack in Cursor with Sinch</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:12:41 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/building-a-communications-stack-in-cursor-with-sinch-2gg2</link>
      <guid>https://dev.to/gunnargrosch/building-a-communications-stack-in-cursor-with-sinch-2gg2</guid>
      <description>&lt;p&gt;A communications integration is rarely slow because of the send itself. The time goes on payload shapes, a different auth scheme for each product, and the distance between something that works in staging and something that works on the phone in your hand.&lt;/p&gt;

&lt;p&gt;I've built enough of these on the Sinch APIs to know where that time goes, which is what makes the &lt;a href="https://cursor.com/marketplace/sinch/sinch-cursor-plugin" rel="noopener noreferrer"&gt;Sinch plugin for Cursor&lt;/a&gt; worth testing properly: I can tell whether the code an agent hands back is right. So I gave myself an afternoon and a demo bank to build against.&lt;/p&gt;

&lt;p&gt;RockBank is that demo bank: A fictional lender whose customers ignore direct-mail payment reminders. The brief was to move the reminders into the channels people actually read, and to keep the whole messaging behind one send path and webhook endpoint so adding a channel is a configuration change instead of a rewrite.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;An SMS payment reminder sent through the Sinch Conversation API&lt;/li&gt;
&lt;li&gt;The same reminder upgraded to RCS, with a verified sender and a "Pay now" button in the thread&lt;/li&gt;
&lt;li&gt;An automated voice reminder if the payment is still outstanding&lt;/li&gt;
&lt;li&gt;Identity verification with a one-time passcode before the payment goes through&lt;/li&gt;
&lt;li&gt;An email confirmation through Mailgun once the payment is complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's messaging, voice, verification, and email behind one send path and inbound webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the build needed
&lt;/h2&gt;

&lt;p&gt;RockBank's code isn't published, so this isn't a clone-and-run. What follows is the account setup the snippets assume, and the same setup you'd need to build your own version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor with the Sinch plugin installed, and a test phone number that belongs to you&lt;/li&gt;
&lt;li&gt;A Conversation API app: Project ID, key ID, key secret, and app ID&lt;/li&gt;
&lt;li&gt;Separate credentials per product: A Voice application key and secret with a number assigned to it, and a Verification app key and secret, which is an app again of its own&lt;/li&gt;
&lt;li&gt;An RCS agent approved for your market, with SMS configured on the same app for fallback. Carrier approval takes time, so start it early. An SMS sender is not a substitute: With no approved agent every reminder lands as plain text.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.sinch.com/docs/10dlc-registration/" rel="noopener noreferrer"&gt;10DLC registration&lt;/a&gt; underway if you send to US numbers. More on that under Gotchas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up the plugin
&lt;/h2&gt;

&lt;p&gt;Install from the Cursor marketplace, or from the command palette:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/add-plugin sinch-cursor-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin has three parts, and they do different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; load Sinch API knowledge into the agent's context before it writes code: endpoints, auth schemes, payload shapes, channel properties. This is the part that decides whether the generated code compiles against the real API or against a plausible-looking invention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commands&lt;/strong&gt; (such as &lt;code&gt;/send-message&lt;/code&gt; and &lt;code&gt;/list-webhooks&lt;/code&gt;) call Sinch APIs directly from chat, with no code in between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MCP servers&lt;/strong&gt;, two of them. &lt;code&gt;sinch&lt;/code&gt; runs &lt;code&gt;npx -y @sinch/mcp&lt;/code&gt; locally and connects the agent to your live account so it can send messages and inspect configuration while you work. &lt;code&gt;sinch-docs&lt;/code&gt; is remote, at &lt;code&gt;developers.sinch.com/mcp&lt;/code&gt;, for searching the documentation. Only the first one takes credentials.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Credentials are the one part of setup worth being precise about. Five variables go in an &lt;code&gt;env&lt;/code&gt; block in &lt;code&gt;~/.cursor/settings.json&lt;/code&gt;, and Cursor needs a restart to pick them up:&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;"env"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"CONVERSATION_PROJECT_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;"your-project-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CONVERSATION_KEY_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;"your-key-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CONVERSATION_KEY_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-key-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CONVERSATION_REGION"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CONVERSATION_APP_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;"your-app-id"&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;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;If the server still reports missing credentials, export the same five variables in your shell profile and do a full quit and relaunch: It runs as &lt;code&gt;npx -y @sinch/mcp&lt;/code&gt; and inherits the environment of the shell Cursor started from.&lt;/p&gt;

&lt;p&gt;These five are the MCP server's, and they cover the Conversation API, so the Voice key and secret never enter Cursor. Voice calls only happen when your own code runs.&lt;/p&gt;

&lt;p&gt;Confirm the connection before writing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/send-message --to=+15551234567 --message="RockBank test"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same command takes a fallback chain, which is a useful preview of what the send path will do later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/send-message --to=+15551234567 --message="RockBank test" --fallback=RCS,SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the send path on the Conversation API
&lt;/h2&gt;

&lt;p&gt;The shape of the send path is the decision everything else depends on. The Conversation API takes one generic message body and transcodes it per channel, and it treats channel selection and fallback as data on the request instead of branches in your code. For RockBank that is the whole design: the reminder goes out over RCS with SMS behind it, voice joins later, and the call site never learns about any of it.&lt;/p&gt;

&lt;p&gt;Worth putting that in the prompt rather than leaving it to be inferred. Ask an agent to "text customers when a payment is due" and you can reasonably get a send path shaped around one channel, because that's what you asked for. State the intent instead, and leave the mechanism to the Skill:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Implement the reminder send path against the Sinch Conversation API. Send RCS with SMS as the fallback, and keep the interface stable so adding a channel later doesn't change any call sites. Also handle delivery receipts and log them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No field names, no endpoint, no auth scheme. The &lt;code&gt;conversation-api&lt;/code&gt; Skill already says how fallback works: Add a &lt;code&gt;channel_priority_order&lt;/code&gt; array and list every channel identity on the recipient. That division is the one to aim for in your own prompts. You describe the behavior you want and the constraints it has to hold to, and the Skill supplies the payload.&lt;/p&gt;

&lt;p&gt;That last clause does real work. Without it you get a send path with no observability, and you find out how it's going the first time a customer says the reminder never arrived. With it, the agent has to account for what happens after &lt;code&gt;messages:send&lt;/code&gt; returns a 200, which is where the interesting failures live.&lt;/p&gt;

&lt;p&gt;What came back is one private method that takes any Conversation API message body, with the fallback expressed as &lt;code&gt;channel_priority_order&lt;/code&gt; and both channel identities on the recipient:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&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;message&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;channel_properties&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&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;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send any Conversation API message over RCS, falling back to SMS.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;properties&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;SMS_SENDER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sms_sender&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sms_sender&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_properties&lt;/span&gt; &lt;span class="ow"&gt;or&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]&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;app_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recipient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identified_by&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel_identities&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&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;RCS&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;identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;to&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;channel&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;SMS&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;identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;to&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="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel_priority_order&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RCS&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;SMS&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;message&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;properties&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel_properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;

    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&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;/v1/projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/messages:send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SinchClientError&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;messages:send failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_reminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&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="n"&gt;to&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;text_message&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&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;Every channel after this one changes the &lt;code&gt;message&lt;/code&gt; body it hands to &lt;code&gt;_send&lt;/code&gt;, and nothing else.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SMS_SENDER&lt;/code&gt; channel property is there because SMS needs an originator unless the app has a default one set. That came from the Skill, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing from the editor
&lt;/h2&gt;

&lt;p&gt;This is the part the MCP server is for. With the client written and the file still open, I asked for a real send:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Send a test reminder through this service to my number, with a due date three days out and an amount of $240.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A live message through a live account, from the editor:&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;"message_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;"01KY292FGM58WNNBETKE1Q4NF8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accepted_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-21T12:05:38.964Z"&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;That response means accepted, not delivered. Delivery happens asynchronously and shows up on the webhook, so the next step is making sure the webhook is actually subscribed to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring the inbound webhook
&lt;/h2&gt;

&lt;p&gt;One endpoint carries both directions of traffic: &lt;code&gt;MESSAGE_DELIVERY&lt;/code&gt; for receipts and &lt;code&gt;MESSAGE_INBOUND&lt;/code&gt; for customer replies. Register it with a Command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/create-webhook --target=https://rockbank.example.com/sinch/callbacks \
  --triggers=MESSAGE_DELIVERY,MESSAGE_INBOUND --secret=$SINCH_WEBHOOK_SECRET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target has to be HTTPS and reachable from the internet, so for local work put a tunnel in front of your handler and register the tunnel URL. The secret is optional on the API, but pass one: It's what signs the callbacks, it has to match the value your handler verifies against, and there is no signature to check without it. An app takes up to five webhooks, and re-registering the same target returns a 400.&lt;/p&gt;

&lt;p&gt;Then check what the app is actually subscribed to, which is not always what you think you asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/list-webhooks
/list-webhook-triggers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth doing before you trust anything downstream. A webhook registered with only &lt;code&gt;MESSAGE_INBOUND&lt;/code&gt; accepts your registration happily, and your delivery receipt handler then sits there and never runs. The send path looks healthy and the delivery log stays empty.&lt;/p&gt;

&lt;p&gt;The handler itself needs a signature check before it looks at the body:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write the FastAPI handler for the &lt;code&gt;/sinch/callbacks&lt;/code&gt; endpoint. Verify the HMAC-SHA256 signature Sinch sends on each callback before processing the body, reject requests with a stale or reused timestamp, and route delivery receipts and inbound messages to separate handlers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sinch signs &lt;a href="https://developers.sinch.com/docs/conversation/callbacks" rel="noopener noreferrer"&gt;callbacks&lt;/a&gt; with HMAC-SHA256 over the raw body, nonce, and timestamp:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;MAX_CALLBACK_AGE_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sinch/callbacks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_callback&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;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&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="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;x-sinch-webhook-signature&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="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&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="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;x-sinch-webhook-signature-nonce&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="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&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="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;x-sinch-webhook-signature-timestamp&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="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;int&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="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bad timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_CALLBACK_AGE_SECONDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stale callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;signed_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_body&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webhook_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;signed_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid signature&lt;/span&gt;&lt;span class="sh"&gt;"&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_delivery_report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;record_delivery&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_delivery_report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;record_inbound_message&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to get right here. The signed string is the raw body, the nonce, and the timestamp joined by dots, in that order, and the digest is base64 rather than hex. Compute it over the raw bytes, not over a re-serialized dict, or it will never match. The key is the secret you set when you created the webhook, and &lt;code&gt;x-sinch-webhook-signature-algorithm&lt;/code&gt; tells you which algorithm was used (&lt;code&gt;HmacSHA256&lt;/code&gt; today).&lt;/p&gt;

&lt;p&gt;A valid signature on its own doesn't tell you the request is fresh. Anyone who captures a signed callback can send it again and the HMAC still checks out, which is what the timestamp and the nonce are there for: Sinch describes the nonce as unique per callback for exactly this purpose. The five-minute window above is my own choice, not a documented value, and it's the cheap half. If you need strict once-only processing, cache the nonces you have already accepted for the length of that window and reject repeats. Idempotent handling is worth having as well, because Sinch retries with exponential backoff and you will legitimately see the same receipt twice, but idempotency is not replay protection: It keeps a replayed callback from corrupting your state without ever telling you it was replayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding RCS
&lt;/h2&gt;

&lt;p&gt;With the send path already channel-agnostic, RCS is a message type and a channel property:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Upgrade the reminder to an RCS rich card with the RockBank verified sender, a payment summary, and a "Pay now" button that opens our hosted payment page. Keep SMS fallback for devices that can't do RCS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The card is a &lt;code&gt;card_message&lt;/code&gt; with a &lt;code&gt;url_message&lt;/code&gt; choice on it, handed to the same &lt;code&gt;_send&lt;/code&gt; as before:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_reminder_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&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;due_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;account_last4&lt;/span&gt;&lt;span class="p"&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;payment_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&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;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;card&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&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;Payment due &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&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;description&lt;/span&gt;&lt;span class="sh"&gt;"&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;Amount: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Account ending &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_last4&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="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url_message&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&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;Pay now&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;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payment_url&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&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="n"&gt;to&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;card_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;channel_properties&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;RCS_WEBVIEW_MODE&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;TALL&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;&lt;code&gt;channel_properties&lt;/code&gt; is a single flat map, so the RCS key and the &lt;code&gt;SMS_SENDER&lt;/code&gt; the fallback needs end up in the same dictionary. That merge is why &lt;code&gt;_send&lt;/code&gt; takes the properties as an argument instead of building them inline. &lt;code&gt;RCS_WEBVIEW_MODE&lt;/code&gt; controls how much of the screen the payment page takes when it opens: &lt;code&gt;FULL&lt;/code&gt;, &lt;code&gt;HALF&lt;/code&gt;, or &lt;code&gt;TALL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two details about RCS that are easy to get wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RCS doesn't process the payment.&lt;/strong&gt; The "Pay now" button is a URL action. It opens your hosted payment page in a webview over the thread, the customer pays there, and control returns to the conversation. What RCS gives you is the verified sender, the branded card, and a customer who never leaves their messaging app. The payment still runs through your payment provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback failures arrive late.&lt;/strong&gt; Every channel you name in &lt;code&gt;channel_priority_order&lt;/code&gt; has to be configured on the app or the request is rejected outright, which is the easy case to debug. The harder case is a configured channel that then fails to deliver: &lt;code&gt;messages:send&lt;/code&gt; returns 200, and the outcome shows up later as a &lt;code&gt;MESSAGE_DELIVERY&lt;/code&gt; callback, with &lt;code&gt;SWITCHING_CHANNEL&lt;/code&gt; marking the point where fallback kicked in. Another reason to get the webhook triggers right before relying on the fallback path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Voice, verification, and email
&lt;/h2&gt;

&lt;p&gt;The remaining four pieces each took one prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice.&lt;/strong&gt; If the reminder goes unanswered and the payment is still outstanding, RockBank calls. A text-to-speech callout needs no call flow server, just a POST to the Voice API:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a voice reminder for payments that are still outstanding two days after the message went out. Use a text-to-speech callout through the Sinch Voice API with our Voice application credentials, and set the caller ID to our registered number.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;place_reminder_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Place a text-to-speech reminder call via the Voice API.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tts&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&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;destination&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;number&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;endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caller_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locale&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;en-US&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/calling/v1/callouts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voice_app_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voice_app_secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;json&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;method&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;ttsCallout&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;ttsCallout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SinchClientError&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;Voice callout failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the auth difference: Voice uses an application key and secret, not the project-level credentials the Conversation API uses. Different product, different auth scheme, and the Skills cover both, so the prompt didn't have to explain either.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;destination&lt;/code&gt; for a phone call is &lt;code&gt;{"type": "number", "endpoint": "+46..."}&lt;/code&gt; in E.164, and the response gives you a &lt;code&gt;callId&lt;/code&gt;. The API reference lists &lt;code&gt;cli&lt;/code&gt; as optional, but set it anyway to a verified number or one assigned from your dashboard: Without a usable caller ID you can get a call ID back for a call that never reaches the phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification.&lt;/strong&gt; A one-time passcode before the payment page accepts anything:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before the payment page accepts anything, verify the customer with a one-time passcode over SMS through the Sinch Verification API. Expose the method so we can switch it per request.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&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;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/verification/v1/verifications&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_verification_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&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;identity&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;number&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;endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&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;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&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;/verification/v1/verifications/id/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&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="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&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="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_verification_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&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;method&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;sms&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;sms&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_verification_auth&lt;/code&gt; is a third set of credentials: Verification authenticates with the key and secret of its own app in the dashboard, so it's neither the project-level keys the Conversation API uses nor the Voice application ones.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;start&lt;/code&gt; returns an &lt;code&gt;id&lt;/code&gt;, and that's what you report the code against. The method values are the part worth knowing: &lt;code&gt;sms&lt;/code&gt; for a passcode by message, &lt;code&gt;callout&lt;/code&gt; for one read out over a phone call, plus &lt;code&gt;flashcall&lt;/code&gt;, &lt;code&gt;seamless&lt;/code&gt;, and &lt;code&gt;whatsapp&lt;/code&gt;. The report body is keyed by the same method, so &lt;code&gt;{"method": "sms", "sms": {"code": "1234"}}&lt;/code&gt; for the message and &lt;code&gt;{"method": "callout", "callout": {"code": "1234"}}&lt;/code&gt; for the call.&lt;/p&gt;

&lt;p&gt;Verifications expire after a few minutes, so treat a 400 on report as "start a new one" rather than as a failed attempt. Switching a customer to &lt;code&gt;callout&lt;/code&gt; when the message doesn't arrive is a second &lt;code&gt;start&lt;/code&gt; call with a different method, and deciding when to offer that is application logic: How long you wait, how many attempts you allow, and whether the customer asks for it or you decide for them. Worth designing deliberately rather than leaving to a retry loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email.&lt;/strong&gt; The confirmation goes out through Mailgun, which is in the same plugin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a payment or payment plan is confirmed, send a confirmation email through Mailgun with the amount, the date, and a reference number.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mailgun is a fourth set of credentials, and the odd one out: it authenticates by domain and key rather than by app, like the other three. That's Basic auth against &lt;code&gt;POST /v3/{domain}/messages&lt;/code&gt;, with &lt;code&gt;api&lt;/code&gt; as the username and an API key as the password. &lt;code&gt;o:testmode=yes&lt;/code&gt; accepts a send without delivering it, which is worth knowing while you wire the confirmation up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start 10DLC registration before you write the sending code.&lt;/strong&gt; Sending SMS to US numbers requires it, approval takes days, and no amount of agent speed compresses that. It's the most common reason an afternoon build becomes a next-week build.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A region mismatch reads like an auth problem.&lt;/strong&gt; All Conversation API URLs are region-specific. If &lt;code&gt;CONVERSATION_REGION&lt;/code&gt; doesn't match where the app lives, you get a 404 or an error that points at your credentials. Check the region in the dashboard before you regenerate a perfectly good key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test the fallback path as hard as the happy path.&lt;/strong&gt; RCS availability varies by market, carrier, and device, and your own phone only proves the happy path. Send to a device with no RCS and confirm the SMS actually arrives. &lt;code&gt;messages:transcode&lt;/code&gt; previews how a card degrades without sending it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The MCP server takes real actions on a real account.&lt;/strong&gt; That's the point of it, and it means pointing it at a test number instead of a list, and thinking about which credentials are sitting in your shell profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep the agent inside the project root.&lt;/strong&gt; During one run the agent searched outside the open folder, found an older implementation of the same feature in a sibling directory, and mirrored that instead of using the Skills. The code worked. It was also the wrong code. Open the project you mean to work in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read what the agent wrote.&lt;/strong&gt; The value of an agent that writes real integration code, instead of handing back an opaque result, is that the code is right there to review. The webhook trigger gap and the missing caller ID were both visible in the diff.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;From the customer's side: A reminder arrives three days before the payment is due. On a device that supports RCS it's a branded card from a verified sender with a "Pay now" button. They tap it, verify with a passcode, pay without leaving the thread, and get an email confirmation. If the payment is still outstanding, the journey follows up with an automated voice reminder.&lt;/p&gt;

&lt;p&gt;By the end, the journey spans messaging, verification, voice, and email, built through the same Cursor workflow. The time the agent gave back was doc-reading time: Working out payload shapes and auth schemes across four products, plus the deploy-and-check cycles that usually sit between a payload being wrong and me finding out about it. Typing was never the slow part.&lt;/p&gt;

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

&lt;p&gt;Install the plugin from the &lt;a href="https://cursor.com/marketplace/sinch/sinch-cursor-plugin" rel="noopener noreferrer"&gt;Cursor marketplace&lt;/a&gt;, export the credentials, relaunch Cursor, and give the agent the Conversation API prompt from above against a test number. The plugin is open source at &lt;a href="https://github.com/sinch/sinch-plugins" rel="noopener noreferrer"&gt;sinch-plugins on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cursor.com/marketplace/sinch/sinch-cursor-plugin" rel="noopener noreferrer"&gt;Sinch Cursor plugin on the Cursor marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sinch/sinch-plugins" rel="noopener noreferrer"&gt;sinch-plugins on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/conversation/" rel="noopener noreferrer"&gt;Sinch Conversation API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/voice/" rel="noopener noreferrer"&gt;Sinch Voice API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/" rel="noopener noreferrer"&gt;Sinch Verification API documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published on &lt;a href="https://sinch.com/blog/how-i-built-communications-stack-cursor-sinch-plugin/" rel="noopener noreferrer"&gt;the Sinch blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cursor</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Yells at Cloud, Episode 2: The Slow Collapse of Civilization</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Mon, 24 Aug 2026 18:56:14 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/yells-at-cloud-episode-2-the-slow-collapse-of-civilization-2177</link>
      <guid>https://dev.to/gunnargrosch/yells-at-cloud-episode-2-the-slow-collapse-of-civilization-2177</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/qTggoTV_g04" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Listen: &lt;a href="https://www.youtube.com/watch?v=qTggoTV_g04" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; · &lt;a href="https://open.spotify.com/episode/0c95OjM90ccesb6oF7cT3I" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://podcasts.apple.com/us/podcast/yells-at-cloud/id6789969050?i=1000785572208" rel="noopener noreferrer"&gt;Apple Podcasts&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;AI agents escaped their sandboxes, exploited Artifactory, and attacked real companies. Is that a genuine safety problem, or just another round of labs marketing how powerful their models are? Danielle covers Grok Bot and the idea of virtual coworkers with access to your accounts. Gunnar looks at Amazon's Region Flex project, where Amazon's retail division is moving workloads because the cloud is running out of physical room. Chris asks what happened to The Climate Pledge as Amazon builds a gas-powered AI data center in Texas.&lt;/p&gt;

&lt;p&gt;Burst Mode: NVIDIA's Nemotron 3.5 Lightning, Amazon destroying books for AI training data, Twitch scraping streamer content because "if it's opt-in, nobody would opt-in," and Matthew's latest take on CloudFormation Express Mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestamps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Main topics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0:00&lt;/code&gt;: Cold open&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2:38&lt;/code&gt;: AI agents escape containment (Chris)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;14:34&lt;/code&gt;: Grok Bot and autonomous virtual coworkers (Danielle)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;22:37&lt;/code&gt;: Project Region Flex and AWS capacity limits (Gunnar)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;30:21&lt;/code&gt;: Amazon's polluting Texas data center (Chris)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Burst Mode&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;36:37&lt;/code&gt;: Burst Mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;36:37&lt;/code&gt;: NVIDIA Nemotron 3.5 Lightning (Danielle)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;42:46&lt;/code&gt;: Amazon book destruction (Chris)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;45:50&lt;/code&gt;: Twitch's honest extraction scheme (Gunnar)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;51:09&lt;/code&gt;: CloudFormation Express Mode (Matthew)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Grok Bot&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.ai/news/introducing-grok-bot" rel="noopener noreferrer"&gt;https://x.ai/news/introducing-grok-bot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Project Region Flex&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.businessinsider.com/amazon-ecommerce-cloud-aws-power-crunch-ai-2026-8" rel="noopener noreferrer"&gt;https://www.businessinsider.com/amazon-ecommerce-cloud-aws-power-crunch-ai-2026-8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Amazon's Texas data center&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://newrepublic.com/post/214111/amazon-data-center-biggest-pollution-source-entire-country" rel="noopener noreferrer"&gt;https://newrepublic.com/post/214111/amazon-data-center-biggest-pollution-source-entire-country&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.distilled.earth/p/scoop-amazon-is-behind-one-of-the" rel="noopener noreferrer"&gt;https://www.distilled.earth/p/scoop-amazon-is-behind-one-of-the&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.yahoo.com/news/us/articles/us-power-grid-risk-18-115217618.html" rel="noopener noreferrer"&gt;https://www.yahoo.com/news/us/articles/us-power-grid-risk-18-115217618.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;NVIDIA Nemotron 3.5 Lightning&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.nvidia.com/blog/nvidia-nemotron-3-5-lightning-delivers-fast-accurate-specialized-task-execution-for-long-running-agents/" rel="noopener noreferrer"&gt;https://developer.nvidia.com/blog/nvidia-nemotron-3-5-lightning-delivers-fast-accurate-specialized-task-execution-for-long-running-agents/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Amazon book destruction&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.404media.co/we-tracked-a-shipment-of-rare-books-it-ended-at-an-amazon-ai-training-facility" rel="noopener noreferrer"&gt;https://www.404media.co/we-tracked-a-shipment-of-rare-books-it-ended-at-an-amazon-ai-training-facility&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Twitch AI training data&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bbc.com/news/articles/cp30pz8d09jo" rel="noopener noreferrer"&gt;https://www.bbc.com/news/articles/cp30pz8d09jo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hosts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gunnar Grosch: &lt;a href="https://gunnargrosch.com" rel="noopener noreferrer"&gt;https://gunnargrosch.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Danielle Heberling: &lt;a href="https://danielleheberling.xyz" rel="noopener noreferrer"&gt;https://danielleheberling.xyz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Matthew Bonig: &lt;a href="https://matthewbonig.com" rel="noopener noreferrer"&gt;https://matthewbonig.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chris Farris: &lt;a href="https://chrisfarris.com" rel="noopener noreferrer"&gt;https://chrisfarris.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Follow the show
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://yellsatcloudpod.com" rel="noopener noreferrer"&gt;https://yellsatcloudpod.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;YouTube: &lt;a href="https://youtube.com/@yellsatcloudpod" rel="noopener noreferrer"&gt;https://youtube.com/@yellsatcloudpod&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bluesky: &lt;a href="https://bsky.app/profile/yellsatcloudpod.com" rel="noopener noreferrer"&gt;https://bsky.app/profile/yellsatcloudpod.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Twitter / X: &lt;a href="https://twitter.com/yellsatcloudpod" rel="noopener noreferrer"&gt;https://twitter.com/yellsatcloudpod&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>climate</category>
      <category>podcast</category>
    </item>
    <item>
      <title>Sinch from the Command Line: Numbers, Calls, Messages, Logs</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:01:52 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/sinch-from-the-command-line-numbers-calls-messages-logs-3eg2</link>
      <guid>https://dev.to/gunnargrosch/sinch-from-the-command-line-numbers-calls-messages-logs-3eg2</guid>
      <description>&lt;p&gt;In &lt;a href="https://gunnargrosch.com/posts/getting-started-with-sinch-functions" rel="noopener noreferrer"&gt;my previous post&lt;/a&gt; on Sinch Functions, I kept reaching for the CLI to test things. At some point I realized I'd been treating &lt;code&gt;@sinch/cli&lt;/code&gt; as a Functions tool when it actually covers the whole product surface: numbers, voice, messaging, fax, SIP trunking, porting. This post goes wider.&lt;/p&gt;

&lt;p&gt;You can do a lot with the Sinch CLI before you've written a line of application code. No Postman, no dashboard tab-switching.&lt;/p&gt;

&lt;p&gt;The sections below are independent: different prerequisites, different credentials, no assumed order. Pick whichever is relevant to what you're building.&lt;/p&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Sinch account (Project ID, Key ID, Key Secret from the dashboard)&lt;/li&gt;
&lt;li&gt;For voice callouts: a Voice App with a Key and Secret&lt;/li&gt;
&lt;li&gt;For messaging: a Conversation App with at least one channel configured&lt;/li&gt;
&lt;li&gt;For webhook testing: a Sinch Function project running locally (see &lt;a href="https://gunnargrosch.com/posts/getting-started-with-sinch-functions" rel="noopener noreferrer"&gt;Getting Started with Sinch Functions&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;For log streaming: a deployed Sinch Function (same Functions post)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installing and authenticating
&lt;/h2&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;span class="nt"&gt;-g&lt;/span&gt; @sinch/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI walks you through Project ID, Key ID, and Key Secret, then two follow-up questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Project ID (from Sinch Dashboard): &amp;lt;your-project-id&amp;gt;
? Key ID (from Project → Access Keys): &amp;lt;your-key-id&amp;gt;
? Key Secret: **********************
✔ API credentials verified!

? Add Voice application credentials? (for voice templates and callbacks) (Y/n)
? Voice Application Key: &amp;lt;your-voice-app-key&amp;gt;
? Voice Application Secret: **********************
✔ Voice credentials verified!

? Install Sinch developer skills for AI assistants? (Claude Code, Copilot, Cursor) (Y/n)

✅ Authenticated
  Project ID: &amp;lt;your-project-id&amp;gt;
  Key ID:     &amp;lt;your-key-id&amp;gt;
  Voice App Key: &amp;lt;your-voice-app-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say yes to Voice Application credentials; you'll need them for callouts later. The Skills prompt only appears if Sinch Skills aren't already installed in your AI coding assistant. If you've done this before, it's skipped automatically. Either way, you don't need them for this walkthrough; more on this at the end.&lt;/p&gt;

&lt;p&gt;If you've logged in before, the CLI detects existing credentials and offers to reuse them.&lt;/p&gt;

&lt;p&gt;All credentials go into your OS keychain: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Nothing lands in a config file in plaintext. &lt;code&gt;sinch auth status&lt;/code&gt; will tell you which storage mode is active.&lt;/p&gt;

&lt;p&gt;Confirm the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ API is healthy

🏥 Health Status:
  Status: healthy
  API URL: https://functions.api.sinch.com
  Project ID: &amp;lt;your-project-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting a number
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers available search &lt;span class="nt"&gt;--region&lt;/span&gt; US &lt;span class="nt"&gt;--type&lt;/span&gt; LOCAL &lt;span class="nt"&gt;--capabilities&lt;/span&gt; SMS VOICE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with all flags passed, the CLI still asks for an optional digit pattern to filter by. This is part of the interactive mode that runs on top of the flags you've already provided. Hit Enter to skip it and get any available number. Add &lt;code&gt;--non-interactive&lt;/code&gt; to skip all optional prompts entirely. Results show as a selectable list with capabilities and cost in your account currency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Found 20 available number(s)
? Select a number to rent (20 available):
❯ +1 202-519-9630  SMS, VOICE  0.80 EUR setup + 0.80 EUR/mo [docs required]
  +1 202-773-9769  SMS, VOICE  0.80 EUR setup + 0.80 EUR/mo [docs required]
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[docs required]&lt;/code&gt; tag on some numbers means regulatory documentation is required. Check the Sinch dashboard for details on what's needed for your region.&lt;/p&gt;

&lt;p&gt;For a specific number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers available check +12025550134
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To rent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers available rent +12025550134
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interactive rent flow walks you through configuration for each capability the number supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Configure SMS for this number? (y/N)
? Voice configuration:
❯ None (configure later)
  RTC — Real-time Communication
  EST — Elastic SIP Trunking
  FAX
? Callback URL for provisioning events (optional):
? Rent +1 202-555-0172? This will activate it on your project. (Y/n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you say yes to SMS, it asks for your SMS service plan ID (find it in the Sinch SMS dashboard) and optionally a campaign ID for US 10DLC compliance. For this walkthrough, say no. You can configure it later from the dashboard. Voice lets you pick RTC, EST (which fetches your SIP trunks for selection), or Fax. Everything you'd normally do across three dashboard tabs, in one wizard. On confirmation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ +1 202-555-0172 rented successfully!

  Phone Number: +1 202-555-0172
  Region:       US
  Type:         LOCAL
  Capabilities: SMS, VOICE
  Cost:         0.80 EUR/month

Manage this number: sinch numbers active get +12025550172
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After renting, confirm it's yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers active list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command prompts for an optional region filter and number type before listing. Leave both blank to see everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Found 3 active number(s)

+1 202-555-0172   [US] LOCAL   SMS, VOICE
+44 20-555-0134   [GB] LOCAL   VOICE
+46 70-555-0180   [SE] MOBILE  SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the full picture on a specific number, pass it in E.164 format (no spaces or dashes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers active get +12025550172
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Number retrieved

ℹ Phone Number: +1 202-555-0172
ℹ Region:       US
ℹ Type:         LOCAL
ℹ Capabilities: SMS, VOICE
ℹ Cost:         0.80 EUR/month
ℹ Next Charge:  8/23/2026
ℹ SMS Service Plan: n/a
ℹ Voice Type:   RTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're done with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch numbers active release +12025550172
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prompts for confirmation unless you pass &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a call
&lt;/h2&gt;

&lt;p&gt;For this you need a Voice App with a Key and Secret. If you set one up during &lt;code&gt;sinch auth login&lt;/code&gt;, you're ready. If not, run &lt;code&gt;sinch auth login&lt;/code&gt; again and say yes when it asks for Voice Application credentials.&lt;/p&gt;

&lt;p&gt;The examples below use separate numbers from the ones in "Getting a number". These are standalone demos, not a continuation of that workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice callouts tts +46700000000 &lt;span class="s2"&gt;"Your shipment has been picked up."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI infers the destination type from the format: E.164 numbers route as PSTN, &lt;code&gt;sip:user@host&lt;/code&gt; routes to SIP, a bare string routes to an in-app endpoint. You don't need to think about it for a standard phone call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ TTS callout queued

ℹ Call ID:     5b5accdf-bd0d-4448-a5d3-4cca076c732b
ℹ Destination: +46700000000
ℹ Voice:       en-US

ℹ Track status with: sinch voice calls get 5b5accdf-bd0d-4448-a5d3-4cca076c732b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few flags worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--voice en-US-Neural2-F&lt;/code&gt; sets a specific voice. Accepts full locale strings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--cli +12039710337&lt;/code&gt; sets the caller ID shown to the recipient.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--dtmf "1w2"&lt;/code&gt; sends DTMF tones after pickup; &lt;code&gt;w&lt;/code&gt; is a 500ms pause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Poll for the result. For a basic TTS callout with no voice function deployed, expect &lt;code&gt;MANAGERHANGUP&lt;/code&gt;: the call connected, the recipient answered, the message played, and the platform ended the call. That's a successful delivery.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice calls get 5b5accdf-bd0d-4448-a5d3-4cca076c732b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Call info retrieved

ℹ Call ID:    5b5accdf-bd0d-4448-a5d3-4cca076c732b
ℹ Status:     FINAL
ℹ Result:     ANSWERED
ℹ Reason:     MANAGERHANGUP
ℹ Duration:   2s
ℹ Domain:     pstn
ℹ To:         +46700000000 (Number)
ℹ Timestamp:  7/23/2026, 3:45:25 PM
ℹ Rate/min:   0.323 EUR
ℹ Total cost: 0.323 EUR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have a voice function deployed to handle the ICE callback, you can keep the call alive, play a menu, collect input, and respond accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending a message
&lt;/h2&gt;

&lt;p&gt;The examples below use separate numbers from the ones in "Getting a number". These are standalone demos, not a continuation of that workflow.&lt;/p&gt;

&lt;p&gt;This requires a Conversation App with at least one channel configured. SMS, WhatsApp, RCS, and others are each set up separately in the Sinch dashboard. Which channels you can actually send through depends on what's configured in your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch conversation send +46700000000 &lt;span class="s2"&gt;"Your driver is 10 minutes away."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't specify a Conversation app with &lt;code&gt;-a &amp;lt;app-id&amp;gt;&lt;/code&gt;, the CLI prompts you to pick one and offers to save it as the default so you're not asked again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Select a Conversation app:
❯ 01KTFHP5SF8SQFSNK5V7NQVKZH — My Conversation App
  Enter app ID manually
? Save as default Conversation App ID? (Y/n)
✅ Default app ID saved. You won't be asked again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then asks which channel to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Select channel:
❯ SMS
  WHATSAPP
  MESSENGER
  INSTAGRAM
  VIBER
  TELEGRAM
  RCS
  LINE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;-c SMS&lt;/code&gt; or &lt;code&gt;-c WHATSAPP&lt;/code&gt; to skip the prompt and target a specific channel directly. Only channels you've configured in the Conversation App will actually work; the others will be accepted by the CLI but fail at the API level.&lt;/p&gt;

&lt;p&gt;Output on success:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Message sent successfully!

ℹ Message ID: 01KY7KS5059DMD3WDFAD3V3J8E
ℹ Recipient:  +46700000000
ℹ Channel:    SMS
ℹ Type:       text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing a webhook locally
&lt;/h2&gt;

&lt;p&gt;Requires a Sinch Function project on your machine (see &lt;a href="https://gunnargrosch.com/posts/getting-started-with-sinch-functions" rel="noopener noreferrer"&gt;Getting Started with Sinch Functions&lt;/a&gt;). Use a dedicated dev Voice App, not one handling live traffic: this rewrites its callback URL.&lt;/p&gt;

&lt;p&gt;From your project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev &lt;span class="nt"&gt;--tunnel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🎯 Starting my-simple-voice-ivr (node)
ℹ Port: 3000

✅ ✅ Server running on http://localhost:3000

[nodemon] starting `npm run build &amp;amp;&amp;amp; sinch-runtime`

Function server running on http://localhost:3000

Detected functions:
   ice (voice): POST http://localhost:3000/ice
   pie (voice): POST http://localhost:3000/pie
   dice (voice): POST http://localhost:3000/dice

Starting tunnel...
Tunnel connected successfully!
✅ Updated voice webhook URL
📱 Test Phone Numbers:
   ☎️  +12025550172
💡 Call any of these numbers to test your voice function!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI starts a local server, opens a Cloudflare tunnel, and rewrites your Voice App's callback URL to point at the tunnel. Real Sinch traffic now hits your machine. From a second terminal, call the test number shown in the startup output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch voice callouts tts +12025550172 &lt;span class="s2"&gt;"Hello from your local function."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ICE callback fires against your local handler. Incoming requests and responses appear in real time in the dev server output, in the same columnar format as &lt;code&gt;sinch functions logs --follow&lt;/code&gt;. File watching is on, so any code change restarts the server automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming logs
&lt;/h2&gt;

&lt;p&gt;After deploying a function, &lt;code&gt;sinch functions logs --follow&lt;/code&gt; streams live request and response data as a columnar table. Each row shows method, status code (green for 2xx, red for 4xx/5xx), duration, and URL. Expand any row to see the full request body, response body, and &lt;code&gt;console.log&lt;/code&gt; output from inside the function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions logs &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ℹ 📋 Logs for function: my-voice-function (✅ Running)

Time        Method  Status  Duration  URL
────────────────────────────────────────────────────────────────────────────────
14:32:39  POST    200  2ms        / [ice]
  ┌ Request
  │ {
  │   "event": "ice",
  │   "callId": "8ce5c8b8-d70f-42f3-ad00-750706738f71",
  │   "cli": "46700000000",
  │   "to": { "type": "did", "endpoint": "+12025550172" },
  │   "domain": "pstn",
  │   ...
  │ }
  ├ Response
  │ {
  │   "instructions": [{ "name": "say", "text": "Press 1 for yes.", "locale": "en-US" }],
  │   "action": { "name": "runMenu", "menus": [...] }
  │ }
  └─────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the function has &lt;code&gt;console.log&lt;/code&gt; calls, the output appears in a &lt;code&gt;Logs&lt;/code&gt; section between &lt;code&gt;Response&lt;/code&gt; and the closing separator. &lt;code&gt;-i&lt;/code&gt; opens a full-screen TUI instead. Arrow keys navigate between requests, Enter expands the detail, Esc closes it, and Q quits.&lt;/p&gt;

&lt;p&gt;Filter by level, text, or time window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions logs &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="nt"&gt;--level&lt;/span&gt; error
sinch functions logs &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="nt"&gt;--search&lt;/span&gt; &lt;span class="s2"&gt;"timeout"&lt;/span&gt;
sinch functions logs &lt;span class="nt"&gt;--since&lt;/span&gt; 30m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're not in a project directory with a &lt;code&gt;sinch.json&lt;/code&gt;, pass the function ID directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions logs &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What else the CLI covers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;More messaging types.&lt;/strong&gt; &lt;code&gt;sinch conversation send&lt;/code&gt; supports cards, carousels, choice messages, location messages, media with captions, and pre-approved templates. Card, carousel, and choice messages are built interactively by default; pass &lt;code&gt;--json-message&lt;/code&gt; with the full message body for non-interactive use. Templates are fetched from the API and presented as a picker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging.&lt;/strong&gt; &lt;code&gt;SINCH_DEBUG=2&lt;/code&gt; on any command shows request method, URL, and status code. &lt;code&gt;SINCH_DEBUG=3&lt;/code&gt; adds response bodies and headers. Useful when you're chasing an auth issue and want to see exactly what's being sent.&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;SINCH_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 sinch voice callouts tts +46700000000 &lt;span class="s2"&gt;"Test."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conference calls.&lt;/strong&gt; &lt;code&gt;sinch voice callouts conference &amp;lt;destination&amp;gt; &amp;lt;conference-id&amp;gt;&lt;/code&gt; dials a number into a conference room, created automatically if it doesn't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mid-call SVAML.&lt;/strong&gt; &lt;code&gt;sinch voice calls update &amp;lt;call-id&amp;gt; --svaml '...'&lt;/code&gt; injects SVAML into an active call without needing a webhook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fax.&lt;/strong&gt; &lt;code&gt;sinch fax send --from &amp;lt;number&amp;gt; --to &amp;lt;number&amp;gt; --file invoice.pdf&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SIP trunking.&lt;/strong&gt; &lt;code&gt;sinch sip trunks&lt;/code&gt; covers trunks, endpoints, ACLs, and credential lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number porting.&lt;/strong&gt; &lt;code&gt;sinch porting orders create&lt;/code&gt; starts a porting order; &lt;code&gt;sinch porting documents upload&lt;/code&gt; handles the paperwork.&lt;/p&gt;

&lt;p&gt;Everything accepts &lt;code&gt;--json&lt;/code&gt; for machine-readable output and &lt;code&gt;--profile &amp;lt;name&amp;gt;&lt;/code&gt; to switch between credential sets. With those two flags, the whole CLI is scriptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth errors on callouts.&lt;/strong&gt; Voice callouts use a separate set of credentials from your Project Key/Secret: the Voice Application Key and Secret. If &lt;code&gt;sinch voice callouts tts&lt;/code&gt; fails with an auth error, you probably skipped that step during &lt;code&gt;sinch auth login&lt;/code&gt;. Run &lt;code&gt;sinch auth login&lt;/code&gt; again and say yes when it asks for Voice Application credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sinch conversation send&lt;/code&gt; fails after picking a channel.&lt;/strong&gt; The CLI accepts any channel in the list, but only channels configured in your Conversation App will go through. Check your app's channel configuration in the Sinch dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sinch numbers active list&lt;/code&gt; output isn't copy-paste compatible with &lt;code&gt;get&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;list&lt;/code&gt; shows numbers in formatted display format (&lt;code&gt;+1 202-555-0172&lt;/code&gt;), but &lt;code&gt;get&lt;/code&gt; expects E.164 (&lt;code&gt;+12025550172&lt;/code&gt;). Strip the spaces and dashes before passing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--non-interactive&lt;/code&gt; hangs in CI.&lt;/strong&gt; The CLI prompts for missing required values if you don't provide them as flags. Pass all required flags explicitly alongside &lt;code&gt;--non-interactive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sinch functions dev --tunnel&lt;/code&gt; rewrites your Voice App's callback URL.&lt;/strong&gt; Use a dedicated dev Voice App, not one handling live traffic. If the tunnel connection fails, check whether port 3000 is already in use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sinch functions logs&lt;/code&gt; says the function wasn't found.&lt;/strong&gt; Log streaming requires a deployed function. If you've only run it locally with &lt;code&gt;sinch functions dev&lt;/code&gt;, deploy it first with &lt;code&gt;sinch functions deploy&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;The whole point of the CLI is that the integration workflow starts in the terminal, not in code. If you want to take that further, an AI coding assistant can drive the CLI commands for you too: describe what you need, and let it figure out which commands to run. The Sinch Skills installed during &lt;code&gt;sinch auth login&lt;/code&gt; give Claude Code, Cursor, and Copilot knowledge of the Sinch APIs, so the same assistant writing your integration code can handle number management, callouts, and message sends alongside it. Same local-first pattern as the &lt;a href="https://gunnargrosch.com/posts/getting-started-with-sinch-functions" rel="noopener noreferrer"&gt;Functions post&lt;/a&gt;: real Sinch infrastructure, no context switching.&lt;/p&gt;

&lt;p&gt;The dashboard is good for configuration you set once. The CLI is better for everything else.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/cli" rel="noopener noreferrer"&gt;Sinch CLI overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gunnargrosch.com/posts/getting-started-with-sinch-functions" rel="noopener noreferrer"&gt;Sinch Functions: Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dashboard.sinch.com" rel="noopener noreferrer"&gt;Sinch dashboard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Curious whether you're using the CLI for anything I didn't cover here. Let me know in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>cli</category>
      <category>voice</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Getting Started with Sinch Functions</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Tue, 21 Jul 2026 21:49:19 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/getting-started-with-sinch-functions-3eeo</link>
      <guid>https://dev.to/gunnargrosch/getting-started-with-sinch-functions-3eeo</guid>
      <description>&lt;p&gt;I've built a lot of voice integrations. Every single one needed a deployment whose only job was to talk to the Sinch API: receive the callback, translate it into SVAML or a Conversation API call, hand off to the rest of the application. Not the interesting part. Just the piece that has to exist because your code and Sinch's network live in different places, with a round trip between them on every call event.&lt;/p&gt;

&lt;p&gt;Sinch Functions moves that piece into the network itself. Your voice and messaging handlers run on the same infrastructure that's already carrying your calls and messages, right where the callbacks originate. This isn't a general-purpose Lambda replacement: the rest of your application, and any compute that isn't glue against Sinch's APIs, stays exactly where it is, whether that's AWS, Azure, or your own infrastructure. Your order lookup, your database writes, your actual business logic: still on Lambda or wherever you already have it. Functions is specifically for the code that exists only to bridge your app and the Sinch network, not a place to run your whole system.&lt;/p&gt;

&lt;p&gt;I've been trying it out and this post walks through getting set up, writing a basic voice function, and deploying it.&lt;/p&gt;

&lt;p&gt;Pricing is usage-based and separate from your Voice and Conversation usage: $0.10 per compute-hour (first hour free each month), $0.05 per GB-month of storage (first 0.1 GB free), $0.05 to $0.15 per GB-month for the database depending on tier (first 0.1 GB free), and $5.00 per month per function if you want it kept always-on instead of scaling to zero. These are the current Standard rates from your project's Billing Overview in the Sinch dashboard; treat them as a snapshot, not a guarantee, given the Alpha status below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sinch Functions is listed as Alpha in the Sinch Build dashboard. Expect the CLI, runtime APIs, pricing, and this walkthrough itself to change before general availability. Treat it as something to experiment with, not something to put in front of production traffic yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  You'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 (the other supported runtime is C#, but this walkthrough uses Node.js)&lt;/li&gt;
&lt;li&gt;A Sinch account with a phone number assigned to a Voice App, ideally a Voice App you don't use for anything real yet; more on why under Running Locally below&lt;/li&gt;
&lt;li&gt;Project ID, Key ID, and Key Secret from the Sinch dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installing the CLI
&lt;/h2&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;span class="nt"&gt;-g&lt;/span&gt; @sinch/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI prompts for your Project ID, Key ID, and Key Secret, then two follow-up questions. Say yes when it asks to add Voice application credentials (Key and Secret from your Voice App); this walkthrough builds a voice function and won't work without them. It'll also offer to install Sinch's developer skills for AI coding assistants like Claude Code and Cursor; that's a genuinely interesting feature but a big enough topic (it installs globally across every agent it finds on your machine) that it deserves its own post rather than a detour here. Everything from this step is stored locally and injected automatically when you run or deploy. You don't reference any of it in code.&lt;/p&gt;

&lt;p&gt;Confirm the connection before moving on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ API is healthy

🏥 Health Status:
  Status: healthy
  API URL: https://functions.api.sinch.com
  Project ID: &amp;lt;your-project-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating your first function
&lt;/h2&gt;

&lt;p&gt;Initialize a project from a template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions init simple-voice-ivr &lt;span class="nt"&gt;--name&lt;/span&gt; my-simple-voice-ivr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This template is also available for C#; pass &lt;code&gt;--runtime csharp&lt;/code&gt; if you'd rather use that. The command walks you through a few prompts: a company name used in the voice prompts, whether to auto-configure voice integration on deploy (say yes, it's what wires up the callback URL automatically), and whether to enable a database (pick "No database" for this one, since a basic IVR greeting doesn't need persistence). It then installs dependencies for you, so there's no separate &lt;code&gt;npm install&lt;/code&gt; step. Once it's done:&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;cd &lt;/span&gt;my-simple-voice-ivr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what the template generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-simple-voice-ivr/
├── .github/
├── .vscode/
├── node_modules/
├── .env
├── AGENTS.md
├── CLAUDE.md
├── README.md
├── function.ts
├── package.json
├── package-lock.json
├── runtime.json
├── sinch.json
├── test.http
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;function.ts&lt;/code&gt; is the only file you need to touch for this walkthrough. &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; are instructions for AI coding assistants working in this project, &lt;code&gt;test.http&lt;/code&gt; is a set of ready-made requests if your editor supports the REST Client format, and &lt;code&gt;runtime.json&lt;/code&gt; and &lt;code&gt;sinch.json&lt;/code&gt; are metadata the CLI manages for you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;function.ts&lt;/code&gt; is where your logic lives. Named exports become HTTP endpoints. The runtime maps them automatically based on the export name.&lt;/p&gt;

&lt;p&gt;Open it before changing anything. The template already generated a complete, working Yes/No IVR: an &lt;code&gt;ice&lt;/code&gt; handler that plays a menu, a &lt;code&gt;pie&lt;/code&gt; handler that responds to the key press and retries on bad input, a &lt;code&gt;dice&lt;/code&gt; handler that logs when the call ends, and a &lt;code&gt;default&lt;/code&gt; handler for a base HTTP endpoint. It's worth reading through once to see the full shape of a real voice function. I'm about to replace most of it with a simpler version to build it back up in stages, not because anything is wrong with what's there.&lt;/p&gt;

&lt;p&gt;Before touching the generated menu logic, replace it with the simplest possible &lt;code&gt;ice&lt;/code&gt; handler, just answering the 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="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;VoiceFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/voice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createUniversalConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;ice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IceRequest&lt;/span&gt;&lt;span class="p"&gt;)&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPANY_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Company&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Hello! Thanks for calling &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;companyName&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="nf"&gt;hangup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;VoiceFunction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ice&lt;/code&gt; is the Incoming Call Event, the first callback Sinch sends when a call arrives, and it's one of four voice callbacks, each firing at a different point in a call's lifecycle: the call rings in (&lt;strong&gt;ICE&lt;/strong&gt;), the call is answered (&lt;strong&gt;ACE&lt;/strong&gt;), the caller presses a key on a menu (&lt;strong&gt;PIE&lt;/strong&gt;), and the call ends (&lt;strong&gt;DICE&lt;/strong&gt;). Each maps to a named export in &lt;code&gt;function.ts&lt;/code&gt;. &lt;code&gt;createIceBuilder()&lt;/code&gt; returns a builder with &lt;code&gt;.say()&lt;/code&gt; and &lt;code&gt;.hangup()&lt;/code&gt; directly, no menu required. &lt;code&gt;createUniversalConfig(context)&lt;/code&gt; reads the &lt;code&gt;COMPANY_NAME&lt;/code&gt; variable you set during &lt;code&gt;init&lt;/code&gt;, so the greeting isn't hardcoded. Don't return raw JSON from voice handlers; use the builders.&lt;/p&gt;

&lt;p&gt;This walkthrough never implements &lt;code&gt;ace&lt;/code&gt;. &lt;code&gt;ice&lt;/code&gt; already runs first and does the real work, so most simple IVRs don't need to act separately on the moment the call is answered. I haven't wired it up myself either, so treat that as an educated guess rather than something confirmed; worth exploring if your use case actually needs it.&lt;/p&gt;

&lt;p&gt;Save that before adding anything else. The next section covers testing it, both locally and with a real call, before the menu gets added on top of it.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts a local server on port 3000 with hot reload. On first run, the CLI asks whether to enable a public tunnel, with four options: enable it once, skip it once, always enable and remember, or never enable and remember. Pick one of the "enable" options. It creates a Cloudflare tunnel and updates your Voice App's callback URL to point at your machine. Real Sinch traffic hits your local function. The tunnel only exists for the lifetime of &lt;code&gt;sinch functions dev&lt;/code&gt;: it tears down when you stop the process, and your Voice App's callback URL reverts on next deploy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This rewrites the callback URL for the Voice App itself, not some separate dev-only layer. Use a dedicated Voice App with its own number for local development, one you never assign to real customer-facing traffic. If you run &lt;code&gt;sinch functions dev&lt;/code&gt; against the same Voice App that's actually handling live calls, you've just rerouted production traffic to your laptop for as long as the dev server is running, so this is worth being deliberate about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once it's up, the CLI prints the number to call directly, something like &lt;code&gt;📱 Test Phone Numbers: +15559876543&lt;/code&gt;. No need to go look it up in the dashboard.&lt;/p&gt;

&lt;p&gt;Test it with curl too:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/ice &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;'{"event":"ice","callId":"test-123","cli":"+15551234567","to":{"type":"number","endpoint":"+15559876543"},"domain":"pstn","originationType":"pstn"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;cat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;to.endpoint&lt;/code&gt; is the Sinch number the CLI just printed, the one being called. &lt;code&gt;cli&lt;/code&gt; is a fake caller ID standing in for whoever's calling; it doesn't need to be a real number for a local curl test. The local dev server needs both &lt;code&gt;event&lt;/code&gt; and &lt;code&gt;callId&lt;/code&gt; (capital I) in the body; leave either out and you'll get "Missing event type in request body."&lt;/p&gt;

&lt;p&gt;That confirms the handler returns valid SVAML, but curl doesn't place an actual call. Since the tunnel is live, call the number you assigned to the Voice App and you should hear the greeting, with the company name you entered during &lt;code&gt;init&lt;/code&gt;, for real before the call hangs up.&lt;/p&gt;

&lt;p&gt;Keep an eye on the terminal where &lt;code&gt;sinch functions dev&lt;/code&gt; is running while you do this. It logs each incoming request and the function's actual response in real time, and it's worth seeing at least once: you'll notice a &lt;code&gt;dice&lt;/code&gt; callback fires automatically when the call ends, even though you haven't written a &lt;code&gt;dice&lt;/code&gt; handler yet. Locally, the runtime handles that gracefully and just logs "DICE callback not implemented, returning 200." Once deployed, the same missing handler gets a 404 ("Function 'dice' not found") instead. Harmless since the call has already ended by the time &lt;code&gt;dice&lt;/code&gt; fires, but worth knowing so a 404 in your production logs doesn't send you looking for a real bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a menu
&lt;/h2&gt;

&lt;p&gt;A call that just answers and hangs up isn't much of an IVR. In &lt;code&gt;function.ts&lt;/code&gt;, add &lt;code&gt;createMenu&lt;/code&gt; to the existing import line (&lt;code&gt;createUniversalConfig&lt;/code&gt; is already there from the minimal version), then replace the body of the &lt;code&gt;ice&lt;/code&gt; method (leave everything else in the file as is) with 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createUniversalConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// inside export default { ... }, replace the ice method with:&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;ice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IceRequest&lt;/span&gt;&lt;span class="p"&gt;)&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPANY_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Company&lt;/span&gt;&lt;span class="dl"&gt;'&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;voiceMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Welcome to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Press 1 for Yes, or press 2 for No.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeatPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please press 1 for Yes, or 2 for No.&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="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(yes)&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="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(no)&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="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxDigits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceMenu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;&lt;code&gt;createMenu()&lt;/code&gt; builds the prompt and DTMF options, and &lt;code&gt;runMenu()&lt;/code&gt; replaces the plain &lt;code&gt;.say()&lt;/code&gt;/&lt;code&gt;.hangup()&lt;/code&gt; chain on the builder with one that waits for input. Call the number again with &lt;code&gt;sinch functions dev&lt;/code&gt; still running: you'll hear the menu prompt, but pressing 1 or 2 won't do anything yet, since nothing handles the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responding to input
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;pie&lt;/code&gt;, the Prompt Input Event, fired when the caller presses a key on the menu. Add &lt;code&gt;createPieBuilder&lt;/code&gt; to the import line, then add this as a second method alongside &lt;code&gt;ice&lt;/code&gt; inside the same &lt;code&gt;export default { ... }&lt;/code&gt; object:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createPieBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// inside export default { ... }, alongside ice:&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;pie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PieRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;menuResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callbackData&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;selectedOption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;menuResult&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;inputMethod&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dtmf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;menuResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createPieBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Great! You selected Yes.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you for calling. Goodbye!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;hangup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createPieBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Okay, you selected No.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you for calling. Goodbye!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;hangup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPANY_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Company&lt;/span&gt;&lt;span class="dl"&gt;'&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;retryMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Welcome to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Press 1 for Yes, or press 2 for No.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(yes)&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="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(no)&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="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxDigits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createPieBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid selection. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;runMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryMenu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads &lt;code&gt;menuResult&lt;/code&gt; off the callback data and responds through &lt;code&gt;createPieBuilder()&lt;/code&gt;, using the same menu-building calls as &lt;code&gt;ice&lt;/code&gt; to re-prompt on invalid input. Curl can't fake a DTMF session, but you don't need to deploy to test it either: with &lt;code&gt;sinch functions dev&lt;/code&gt; still running, the tunnel already points your Voice App's callback URL at your machine, so call the number now and press 1 or 2 for real. Hot reload picks up changes to &lt;code&gt;pie&lt;/code&gt; without restarting the dev server.&lt;/p&gt;

&lt;p&gt;One more worth adding: &lt;code&gt;dice&lt;/code&gt;, the Disconnect Call Event, fired after the call has already ended. There's nothing to respond with here, it's just a hook for logging:&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;async&lt;/span&gt; &lt;span class="nf"&gt;dice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DiceRequest&lt;/span&gt;&lt;span class="p"&gt;)&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;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&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;callbackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="s2"&gt;`Call ended - reason: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, duration: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;duration&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;The reason to implement it is basic operational visibility: knowing why and how long calls actually ran, whether callers hang up early, get disconnected, or make it through the whole menu, is the kind of thing you want logged for any real IVR, independent of anything else. It also means you won't see the dashboard's error rate sitting at a nonzero number for a function that's actually working fine, since every call would otherwise fire a 404 once &lt;code&gt;dice&lt;/code&gt; runs without a handler (covered under Running Locally above).&lt;/p&gt;

&lt;p&gt;Here's the complete &lt;code&gt;function.ts&lt;/code&gt; at this point, all four handlers assembled:&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;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;VoiceFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/voice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createPieBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;ice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IceRequest&lt;/span&gt;&lt;span class="p"&gt;)&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPANY_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Company&lt;/span&gt;&lt;span class="dl"&gt;'&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;voiceMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Welcome to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Press 1 for Yes, or press 2 for No.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeatPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please press 1 for Yes, or 2 for No.&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="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(yes)&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="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(no)&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="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxDigits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createIceBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceMenu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;pie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PieRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;menuResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callbackData&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;selectedOption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;menuResult&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;inputMethod&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dtmf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;menuResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createPieBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Great! You selected Yes.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you for calling. Goodbye!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;hangup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createPieBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Okay, you selected No.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you for calling. Goodbye!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;hangup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPANY_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Company&lt;/span&gt;&lt;span class="dl"&gt;'&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;retryMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Welcome to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Press 1 for Yes, or press 2 for No.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(yes)&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="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;return(no)&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="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxDigits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createPieBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid selection. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="nf"&gt;runMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryMenu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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="p"&gt;},&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;dice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DiceRequest&lt;/span&gt;&lt;span class="p"&gt;)&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;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&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;callbackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&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="s2"&gt;`Call ended - reason: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callbackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, duration: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;duration&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="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;VoiceFunction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ice&lt;/code&gt;, &lt;code&gt;pie&lt;/code&gt;, and &lt;code&gt;dice&lt;/code&gt; are all confirmed working end to end: the menu, both valid answers, bad input triggering the retry, and &lt;code&gt;dice&lt;/code&gt; returning 200 instead of a 404.&lt;/p&gt;

&lt;p&gt;Not included: &lt;code&gt;default&lt;/code&gt;, a method name from the original generated template that becomes a base HTTP endpoint (not to be confused with the &lt;code&gt;export default&lt;/code&gt; wrapping the whole object). Add it back if you want a custom endpoint; it's not needed for the IVR itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration and secrets
&lt;/h2&gt;

&lt;p&gt;Non-sensitive configuration like company names, support numbers, or feature flags goes in &lt;code&gt;sinch.json&lt;/code&gt; under &lt;code&gt;variables&lt;/code&gt;:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-simple-voice-ivr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodejs20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variables"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"SUPPORT_NUMBER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+15551234567"&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;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;&lt;code&gt;init&lt;/code&gt; already wrote a &lt;code&gt;.env&lt;/code&gt; for you, populated with your Project ID, Key ID, Voice Application Key, and the company name you entered. The actual secrets (Key Secret, Voice Application Secret) are left blank there on purpose since they're pulled from your OS keychain at runtime, not stored in the file.&lt;/p&gt;

&lt;p&gt;Here's the part to pay attention to: &lt;strong&gt;the generated &lt;code&gt;.gitignore&lt;/code&gt; does not exclude &lt;code&gt;.env&lt;/code&gt;.&lt;/strong&gt; It covers &lt;code&gt;node_modules/&lt;/code&gt;, &lt;code&gt;package-lock.json&lt;/code&gt;, and a few log/OS files, but not &lt;code&gt;.env&lt;/code&gt;. If you &lt;code&gt;git init &amp;amp;&amp;amp; git add .&lt;/code&gt; in a fresh project, your Project ID and Key ID go into the repo in plaintext. Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; yourself before your first commit.&lt;/p&gt;

&lt;p&gt;For your own secrets, like a third-party API key, declare the name in &lt;code&gt;.env&lt;/code&gt; with an empty value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ELEVENLABS_API_KEY=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set the actual value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch secrets add ELEVENLABS_API_KEY sk-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locally, values go into your OS keychain. In production, the platform uses an encrypted secret store. Access them in code:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUniversalConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sinch/functions-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUniversalConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requireSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ELEVENLABS_API_KEY&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;&lt;code&gt;requireSecret&lt;/code&gt; throws at startup if the value is missing. Good behavior for anything your function can't run without.&lt;/p&gt;

&lt;p&gt;To attach a debugger while testing any of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions dev &lt;span class="nt"&gt;--debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the Node.js inspector on port 9229. Connect from VS Code, Cursor, or WebStorm with F5, or attach manually to &lt;code&gt;ws://localhost:9229&lt;/code&gt;. Secrets load automatically from your OS keychain in this mode too, so you don't need to re-enter anything to debug a handler that calls &lt;code&gt;requireSecret&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy asks two more questions first. The first is whether to generate or update documentation. Say yes; it's more useful than it sounds. It reads your actual code and generates a full write-up: an overview, a step-by-step caller experience walkthrough, key features, and a call flow diagram showing the branches through &lt;code&gt;ice&lt;/code&gt; and &lt;code&gt;pie&lt;/code&gt; as boxes and arrows. You can view it anytime from the function's Documentation tab in the dashboard, or in the generated README.md.&lt;/p&gt;

&lt;p&gt;The second question is what access level the function should have. Pick &lt;strong&gt;Always Private (internal access only, Sinch services)&lt;/strong&gt; rather than plain Private: this function only ever receives callbacks from Sinch's own platform, so there's no reason to expose it publicly. The "Always" matters here. Plain Private only applies to this one deploy and asks again next time. Always Private is saved to the project's &lt;code&gt;sinch.json&lt;/code&gt; (as &lt;code&gt;"accessPreference": "always-private"&lt;/code&gt;), so you don't have to get the answer right on every future redeploy.&lt;/p&gt;

&lt;p&gt;The CLI then lists every configuration variable and secret it's about to ship, packages your code, and uploads it.&lt;/p&gt;

&lt;p&gt;Your function ends up live at a URL like &lt;code&gt;https://my-simple-voice-ivr.private.fn.sinch.com&lt;/code&gt;; the exact format depends on the access level you picked. The platform updates your Voice App's callback URL automatically, pointing it away from the dev tunnel and at the deployed function. Nothing to configure manually.&lt;/p&gt;

&lt;p&gt;Call the number again to confirm production behaves the same as local dev did: the Yes/No menu from &lt;code&gt;ice&lt;/code&gt;, then a response from &lt;code&gt;pie&lt;/code&gt; after pressing 1 or 2.&lt;/p&gt;

&lt;p&gt;None of the choices you made here are permanent. The function's Settings tab in the dashboard lets you change access level, database tier, always-on, and every configuration variable and secret after the fact, no redeploy needed for most of it. It also has a Danger Zone with a one-click "Delete function," worth knowing about once you're done experimenting, given the Alpha status.&lt;/p&gt;

&lt;p&gt;To follow logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions logs &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each request streams in fully expanded: time, method, status, duration, and the complete request and response bodies, no navigation needed. Logs live here, not in CloudWatch or whatever you've already got wired up for the rest of your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port 3000 already in use.&lt;/strong&gt; &lt;code&gt;sinch functions dev&lt;/code&gt; defaults to port 3000, and that's a common default for a lot of other local dev servers too. If something else already has it, run &lt;code&gt;sinch functions dev --port 8080&lt;/code&gt; (or any free port) instead, and point curl at the new port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunnel won't establish.&lt;/strong&gt; &lt;code&gt;sinch functions dev&lt;/code&gt; fails to create the Cloudflare tunnel. Usually a network or firewall issue blocking outbound connections; retry, or run with &lt;code&gt;--no-tunnel&lt;/code&gt; and test with curl against &lt;code&gt;localhost&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Missing secret" at runtime versus at startup.&lt;/strong&gt; &lt;code&gt;config.requireSecret(...)&lt;/code&gt; throws immediately on cold start if the secret was never set with &lt;code&gt;sinch secrets add&lt;/code&gt;. A runtime error deeper in your handler instead usually means the secret exists but the key name doesn't match what your code requested; check for typos between the &lt;code&gt;.env&lt;/code&gt; declaration and the &lt;code&gt;requireSecret&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calls connect but nothing happens.&lt;/strong&gt; If the caller hears silence or a generic carrier message instead of your greeting, the Voice App's callback URL likely isn't pointed at your deployed function. This fails silently from the caller's side; check the Voice App configuration in the dashboard against your deployment output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cleaning up
&lt;/h2&gt;

&lt;p&gt;When you're done, delete the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch functions delete &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;del&lt;/code&gt; also works, and it asks for confirmation unless you pass &lt;code&gt;-f&lt;/code&gt;.) Find the function ID with &lt;code&gt;sinch functions list&lt;/code&gt; if you don't have it handy, or in the dashboard's Overview tab. Same result as the dashboard's Settings → Danger Zone → Delete function button, either way works. Nothing else to clean up locally beyond deleting the project directory itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else you can build
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;simple-voice-ivr&lt;/code&gt; template gets you started, but the platform supports more than IVR menus. Browse what's available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sinch templates list
sinch templates list &lt;span class="nt"&gt;--runtime&lt;/span&gt; node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard lists four popular use cases: phone tree IVRs (press 1 for sales, 2 for support, built with SVAML actions like the one above), AI voice agents, number masking (connecting two parties without exposing either real phone number), and SMS autoresponders (custom or AI-powered replies to inbound messages). Some templates ship in both runtimes; &lt;code&gt;simple-voice-ivr&lt;/code&gt; and &lt;code&gt;number-masking&lt;/code&gt; each have a Node.js and a C# version.&lt;/p&gt;

&lt;p&gt;AI voice agents are where it gets interesting, and there's more than one way to build one. &lt;code&gt;11labs-leadgen&lt;/code&gt; and &lt;code&gt;11labs-support&lt;/code&gt; connect calls to ElevenLabs conversational agents, but both are C# templates today. On the Node.js side, &lt;code&gt;realtime-deepgram&lt;/code&gt;, &lt;code&gt;realtime-google&lt;/code&gt;, and &lt;code&gt;realtime-openai&lt;/code&gt; each wire a single WebSocket up to a different provider's voice API (Deepgram's Nova STT plus Aura TTS, or native audio-in/audio-out with Gemini Live or the OpenAI Realtime API, no separate STT/TTS needed for the latter two). Picking one of those and building a real voice agent on it is specific enough to deserve its own post rather than a mention here.&lt;/p&gt;

&lt;p&gt;The Node.js runtime also gives you custom HTTP endpoints, Conversation API webhooks, and a startup hook for initialization logic. The context object includes a persistent cache shared across invocations, durable blob storage, and an embedded SQLite database. None of it requires provisioning, but all of it is still scoped to Sinch-triggered work: a place to react to calls and messages, not to relocate your API layer.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/concepts/overview" rel="noopener noreferrer"&gt;Sinch Functions overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/runtimes/nodejs" rel="noopener noreferrer"&gt;Node.js runtime reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/templates" rel="noopener noreferrer"&gt;Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/functions/functions/concepts/configuration-secrets" rel="noopener noreferrer"&gt;Configuration and secrets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you've got a Sinch integration that's really just gluing callbacks together, curious whether this looks worth trying. Let me know in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>compute</category>
      <category>typescript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Yells at Cloud, Episode 1: Generally Available</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:10:26 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/yells-at-cloud-episode-1-generally-available-350h</link>
      <guid>https://dev.to/gunnargrosch/yells-at-cloud-episode-1-generally-available-350h</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/lWUu3V6dxpc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Listen: &lt;a href="https://www.youtube.com/watch?v=lWUu3V6dxpc" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; · &lt;a href="https://open.spotify.com/episode/6FxW0YCX4CX7eNpBAtqbET" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://podcasts.apple.com/us/podcast/generally-available/id6789969050?i=1000776391774" rel="noopener noreferrer"&gt;Apple Podcasts&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;AWS shipped Lambda MicroVMs, Firecracker-based sandboxes that snap back to life in milliseconds, and AJ cannot figure out who they're actually for. The pricing charges you two to three times more than it charges AWS' own services built on top of it, which is a choice. Matthew explained why CloudFormation Express Mode breaks a contract CFN has held since day one: it reports a stack as done before the resources are actually available, and disables rollback by default. Danielle brought actual good news: Cloudflare's temporary accounts let agents deploy and test without ever touching your credentials, which Matthew promptly called growth hacking. Chris covered AWS WAF's new AI traffic monetization. Publishers can now charge crawlers per request instead of just blocking them. AJ called it too little too late. And Gunnar made the case that Microsoft and AWS launching billion-dollar forward-deployed engineering programs is basically an admission that enterprise AI adoption is stuck and the tools don't work without hand-holding.&lt;/p&gt;

&lt;p&gt;Burst Mode: Claude Sonnet 5 is "available" on Bedrock in the sense that Danielle got an AccessDeniedException and was told to call sales. Matthew on IAM Identity Center finally getting programmatic access: what took so long? AJ on Anthropic renegotiating with Amazon to token-based pricing and what that means as open-weight models close the gap. Chris was at the AWS DC Summit when the Secretary of Energy got roundly booed for claiming AI data centers won't materially impact energy consumption. And Gunnar on a paper showing that multi-agent teams consistently underperform their smartest member. Not because of disagreement. Because of dilution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestamps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Main topics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0:00&lt;/code&gt;: Cold open&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0:52&lt;/code&gt;: Lambda MicroVMs (AJ)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;14:05&lt;/code&gt;: CloudFormation / CDK Express Mode (Matthew)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;25:01&lt;/code&gt;: Cloudflare Temporary Accounts (Danielle)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;31:37&lt;/code&gt;: AWS WAF AI Traffic Monetization (Chris)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;39:06&lt;/code&gt;: Microsoft Frontier Co. &amp;amp; AWS Forward Deployed Engineering (Gunnar)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Burst Mode&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;49:07&lt;/code&gt;: Burst Mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;49:30&lt;/code&gt;: Claude Sonnet 5 on Bedrock: "available" (Danielle)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;51:13&lt;/code&gt;: IAM Identity Center programmatic access (Matthew)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;52:21&lt;/code&gt;: Anthropic / Amazon token pricing renegotiation (AJ)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;55:10&lt;/code&gt;: AWS DC Summit: Secretary of Energy gets booed (Chris)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;58:17&lt;/code&gt;: Multi-agent systems underperform their best member (Gunnar)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Lambda MicroVMs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/lambda/lambda-microvms/" rel="noopener noreferrer"&gt;https://aws.amazon.com/lambda/lambda-microvms/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CloudFormation / CDK Express Mode&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/aws-cloudformation-cdk/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/06/aws-cloudformation-cdk/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Temporary Accounts&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/temporary-accounts/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/temporary-accounts/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS WAF AI Traffic Monetization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/aws-waf-adds-ai-traffic-monetization-capability-to-help-content-owners-charge-ai-bots-for-content-access/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/aws/aws-waf-adds-ai-traffic-monetization-capability-to-help-content-owners-charge-ai-bots-for-content-access/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Microsoft Frontier Co. &amp;amp; AWS Forward Deployed Engineering&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blogs.microsoft.com/blog/2026/07/02/microsoft-frontier-company-ai-engineering-that-amplifies-and-protects-your-intelligence/" rel="noopener noreferrer"&gt;https://blogs.microsoft.com/blog/2026/07/02/microsoft-frontier-company-ai-engineering-that-amplifies-and-protects-your-intelligence/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/apn/introducing-forward-deployed-engineering-for-partners-winning-the-future-of-enterprise-ai/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/apn/introducing-forward-deployed-engineering-for-partners-winning-the-future-of-enterprise-ai/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/ai-and-ml/2026/07/06/even-banks-and-hyperscalers-are-now-sounding-the-alarm-about-the-ai-bubble/5266123" rel="noopener noreferrer"&gt;https://www.theregister.com/ai-and-ml/2026/07/06/even-banks-and-hyperscalers-are-now-sounding-the-alarm-about-the-ai-bubble/5266123&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Claude Sonnet 5 on Bedrock&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/introducing-claude-sonnet-5-on-aws-anthropics-most-capable-sonnet-model/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/machine-learning/introducing-claude-sonnet-5-on-aws-anthropics-most-capable-sonnet-model/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IAM Identity Center programmatic access&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/aws-iam-identity-center-account-access-customer-managed-apps/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/06/aws-iam-identity-center-account-access-customer-managed-apps/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Anthropic / Amazon token pricing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thenextweb.com/news/amazon-anthropic-token-pricing-openai-alternative" rel="noopener noreferrer"&gt;https://thenextweb.com/news/amazon-anthropic-token-pricing-openai-alternative&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent systems paper&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2602.01011" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2602.01011&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hosts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gunnar Grosch: &lt;a href="https://gunnargrosch.com" rel="noopener noreferrer"&gt;https://gunnargrosch.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Danielle Heberling: &lt;a href="https://danielleheberling.xyz" rel="noopener noreferrer"&gt;https://danielleheberling.xyz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AJ Stuyvenberg: &lt;a href="https://aaronstuyvenberg.com" rel="noopener noreferrer"&gt;https://aaronstuyvenberg.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Matthew Bonig: &lt;a href="https://matthewbonig.com" rel="noopener noreferrer"&gt;https://matthewbonig.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chris Williams: &lt;a href="https://mistwire.com" rel="noopener noreferrer"&gt;https://mistwire.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Follow the show
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://yellsatcloudpod.com" rel="noopener noreferrer"&gt;https://yellsatcloudpod.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;YouTube: &lt;a href="https://youtube.com/@yellsatcloudpod" rel="noopener noreferrer"&gt;https://youtube.com/@yellsatcloudpod&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bluesky: &lt;a href="https://bsky.app/profile/yellsatcloudpod.com" rel="noopener noreferrer"&gt;https://bsky.app/profile/yellsatcloudpod.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Twitter / X: &lt;a href="https://twitter.com/yellsatcloudpod" rel="noopener noreferrer"&gt;https://twitter.com/yellsatcloudpod&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>podcast</category>
      <category>ai</category>
      <category>bedrock</category>
    </item>
    <item>
      <title>Picking a Phone Verification Method: SMS, Flash Call, Phone Call, and Data Verification</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Sun, 14 Jun 2026 21:34:07 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/picking-a-phone-verification-method-sms-flash-call-phone-call-and-data-verification-2a5h</link>
      <guid>https://dev.to/gunnargrosch/picking-a-phone-verification-method-sms-flash-call-phone-call-and-data-verification-2a5h</guid>
      <description>&lt;p&gt;When your app needs to confirm that a user actually owns the phone number they gave you, the pattern looks the same from the outside: send something to the device, user usually confirms it. Under that surface, there are four distinct approaches using phone and carrier networks, each with different security characteristics, user experiences, and requirements. The right one depends on your context.&lt;/p&gt;

&lt;p&gt;If you want the implementation side, the &lt;a href="https://developers.sinch.com/docs/verification/introduction" rel="noopener noreferrer"&gt;Sinch Verification API&lt;/a&gt; is a good starting point. I've covered the code in detail in &lt;a href="https://dev.to/gunnargrosch/phone-based-user-verification-in-typescript-and-python-ddb"&gt;Phone-Based User Verification in TypeScript and Python&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four methods
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;User action&lt;/th&gt;
&lt;th&gt;Requires mobile data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMS OTP&lt;/td&gt;
&lt;td&gt;Text message&lt;/td&gt;
&lt;td&gt;Read and type a numeric code (auto-fill possible on Android)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash Call&lt;/td&gt;
&lt;td&gt;Missed call (caller ID is the code)&lt;/td&gt;
&lt;td&gt;None (Android SDK) / Enter caller ID (iOS, web)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone Call Verification&lt;/td&gt;
&lt;td&gt;Inbound phone call&lt;/td&gt;
&lt;td&gt;Listen and type a numeric code&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Verification&lt;/td&gt;
&lt;td&gt;Carrier network check&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The differences matter more than they appear in that table.&lt;/p&gt;

&lt;h3&gt;
  
  
  SMS OTP
&lt;/h3&gt;

&lt;p&gt;The default choice for most apps. It works on any phone, any network, over Wi-Fi or cellular. Your users already know what to do with a six-digit code. Delivery is global, the integration is straightforward, and it pairs with any backend.&lt;/p&gt;

&lt;p&gt;On Android, the SMS Retriever API makes auto-fill possible: the mobile SDK can read the incoming message and fill the code without user input, if your app implements it. Most apps don't, so users typically still read and type the code manually.&lt;/p&gt;

&lt;p&gt;The trade-off is that a code the user can read is a code that can be relayed, whether by accident or by a phishing page. For most consumer flows that's an acceptable trade. For account recovery or financial transactions, you may want to weigh methods where no code changes hands at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flash Call
&lt;/h3&gt;

&lt;p&gt;A call is placed to the user's number and immediately disconnected. The incoming caller ID is the verification code. On Android, the mobile SDK can intercept the caller ID automatically, completing verification without any user input. On iOS and web, the user reads the incoming number from their recent calls and enters it manually.&lt;/p&gt;

&lt;p&gt;On Android, both SMS and Flash Call can complete without user input. The difference is that Flash Call never delivers a visible code at all. There's nothing to relay to a phishing page, misread, or enter incorrectly. On iOS the UX is more similar to SMS since the user still has to retrieve and enter a value, just from the call log instead of a text. Flash Call is still PSTN-based, so it shares the same SIM-level exposure as SMS. The specific benefit is phishing-resistance, not a broader security upgrade.&lt;/p&gt;

&lt;p&gt;Flash Call is typically priced at a fixed rate per attempt, while SMS pricing varies by destination country and operator. In markets where SMS termination is expensive, Flash Call can be meaningfully cheaper at scale. Voice calls typically cost more than SMS, and Data Verification is priced separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phone Call Verification
&lt;/h3&gt;

&lt;p&gt;A text-to-speech call reads the code aloud. By default the user listens and types it in. Most providers also support prompting the user to press a digit on the keypad to confirm, which helps bypass voicemail: if the user doesn't answer, the call goes to voicemail and the code is spoken to the wrong listener. The keypress prompt ensures a human answered before the code is read out.&lt;/p&gt;

&lt;p&gt;Choose it for coverage: it reaches numbers that SMS can't. Landlines, VoIP numbers, users in regions where SMS delivery is consistently unreliable. For mobile-first apps, it adds friction without much benefit over SMS. For apps where users might be verifying a non-mobile number or where SMS delivery is unreliable in your target market, it closes a gap the other methods leave open.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Verification
&lt;/h3&gt;

&lt;p&gt;Your backend calls the verification API with the user's phone number. The carrier authenticates the device through its network infrastructure, with no visible interaction. The result comes back in the API response. If it succeeds, verification is done before the user does anything.&lt;/p&gt;

&lt;p&gt;There's no code for the user to handle at any point. The constraint is cellular data: it must be enabled and available on the device. Modern mobile OSes can route the authentication request over the cellular interface even when Wi-Fi is connected, but if cellular data is off or unavailable, the carrier can't authenticate the device and the request fails. A second constraint worth knowing: on dual-SIM devices, the carrier authenticates based on the SIM handling data traffic. If that SIM is different from the number being verified, the check fails. You need a fallback either way.&lt;/p&gt;

&lt;p&gt;One more practical note: on Sinch, Data Verification isn't self-serve. Enabling it requires carrier routing agreements, so you'll need to contact your account manager before it's available. Plan for that lead time if you're building it into your architecture.&lt;/p&gt;

&lt;p&gt;Data Verification is a good first attempt for mobile apps where you control the client environment, but it can't stand alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pick
&lt;/h2&gt;

&lt;p&gt;The decision comes down to a few questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this a mobile app or a web app?&lt;/strong&gt; For web, SMS is the baseline. Flash Call and Phone Call Verification are useful as fallbacks but neither delivers the native SDK experience that makes them frictionless on mobile. For mobile apps, a practical default: Flash Call on Android (the SDK intercepts automatically, zero user input), SMS on iOS (Flash Call is less streamlined there). Layer Data Verification on top of that if your carrier coverage allows it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much friction can your flow afford?&lt;/strong&gt; Signup confirmation can tolerate a code-entry step. A checkout flow or account recovery step benefits from fewer interruptions. Data Verification and Flash Call (with the Android SDK) add zero friction for the user. If reducing drop-off at the verification step matters to your conversion, start there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who are your users and where are they?&lt;/strong&gt; SMS delivery is not uniform globally. In some regions, high termination costs make SMS pricing volatile at scale, and Flash Call's fixed per-attempt pricing is worth considering. In others, carrier support for CLI delivery or Data Verification varies. If you're building for a specific geography, check delivery rates and method availability for that market before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do your users have landlines or VoIP numbers?&lt;/strong&gt; If yes, Phone Call Verification is the only method that reaches them. Build it as a fallback option even if it's not your primary path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallback chains
&lt;/h2&gt;

&lt;p&gt;No single method works for every user in every context. Build a fallback chain rather than picking one and moving on.&lt;/p&gt;

&lt;p&gt;The general sequence: attempt Data Verification first for mobile users (best UX, no code handling), fall back to Flash Call or SMS if it fails or the user is on Wi-Fi, and offer Phone Call Verification as a last resort for users who don't receive the first attempt. On the web, start with SMS and offer Phone Call Verification as the fallback path, surfaced as a "didn't receive the code?" option in your UI.&lt;/p&gt;

&lt;p&gt;If you're using a mobile SDK, it will typically provide a callback you can hook into to trigger a fallback automatically when the primary method fails. If you're integrating via the REST API directly, you implement the fallback yourself: catch the failure response from &lt;code&gt;start&lt;/code&gt;, then issue a new request with a different method value.&lt;/p&gt;

&lt;p&gt;The two-step API is the same regardless of method: &lt;code&gt;start&lt;/code&gt; to trigger delivery, &lt;code&gt;report&lt;/code&gt; to validate what the user confirms. Switching between methods is a one-field change in the request body, which keeps the fallback logic straightforward to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;SMS OTP is broadly compatible and the right default for most apps. Flash Call eliminates the code-entry step and offers fixed-rate pricing. Phone Call Verification covers landlines and numbers that SMS can't reach. Data Verification is the lowest friction option for mobile but requires cellular data, a fallback, and provisioning through your account manager.&lt;/p&gt;

&lt;p&gt;For most apps, the answer isn't picking one but combining them: Data Verification or Flash Call as the primary path for mobile, SMS as the reliable fallback, Phone Call Verification as the last resort. That covers most users and handles the edge cases where your first choice fails.&lt;/p&gt;

&lt;p&gt;If you want the backend implementation for any of these methods, including TypeScript and Python examples for the Sinch Verification API, that's covered in &lt;a href="https://dev.to/gunnargrosch/phone-based-user-verification-in-typescript-and-python-ddb"&gt;Phone-Based User Verification in TypeScript and Python&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/introduction" rel="noopener noreferrer"&gt;Sinch Verification API introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/api-reference/" rel="noopener noreferrer"&gt;Sinch Verification API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gunnargrosch/phone-based-user-verification-in-typescript-and-python-ddb"&gt;Phone-Based User Verification in TypeScript and Python&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What verification method are you using, and what drove the choice? Let me know in the comments.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Phone-Based User Verification in TypeScript and Python</title>
      <dc:creator>Gunnar Grosch</dc:creator>
      <pubDate>Wed, 10 Jun 2026 13:07:26 +0000</pubDate>
      <link>https://dev.to/gunnargrosch/phone-based-user-verification-in-typescript-and-python-ddb</link>
      <guid>https://dev.to/gunnargrosch/phone-based-user-verification-in-typescript-and-python-ddb</guid>
      <description>&lt;p&gt;A user signs up for your app. You need to confirm the phone number they entered actually belongs to them. A returning user wants to log in without a password. An account change needs a second factor before it goes through.&lt;/p&gt;

&lt;p&gt;The underlying pattern is always the same: send a code to the device, wait for the user to confirm it.&lt;/p&gt;

&lt;p&gt;The Sinch Verification API handles this with a two-step flow. Your backend calls start to trigger code delivery, and report to validate what the user submits. It supports four methods: SMS, FlashCall (a missed call where the incoming caller ID is the code), voice (text-to-speech), and Data Verification (silent authentication through the carrier's network, no user input at all). The payload shape is the same for all four, so switching methods is a one-field change.&lt;/p&gt;

&lt;p&gt;This post covers the backend piece. Whether your frontend is a web app, iOS, or Android, the pattern is the same: your client calls your API, your API calls Sinch. A mobile app calling a Lambda function, a web form submitting to an Express server, a Next.js API route. The code here works for all of them.&lt;/p&gt;

&lt;p&gt;This post uses the official Sinch Node.js SDK. Python and no-dependency Node.js examples are in the raw HTTP section at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You'll need a Sinch account (&lt;a href="https://community.sinch.com/t5/Customer-Dashboard/How-to-sign-up-for-your-free-Sinch-account/ta-p/8058" rel="noopener noreferrer"&gt;sign up here&lt;/a&gt;, a trial account is enough to test, see &lt;a href="https://www.sinch.com/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; for details) and Node.js 18+ or Python 3.9+.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dashboard.sinch.com" rel="noopener noreferrer"&gt;Sinch Dashboard&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Verification app.&lt;/strong&gt; Go to &lt;a href="https://dashboard.sinch.com/verification/apps" rel="noopener noreferrer"&gt;Verification &amp;gt; Apps&lt;/a&gt; and click &lt;strong&gt;New app&lt;/strong&gt;. Enter a name and optional description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy your App Key and App Secret.&lt;/strong&gt; They appear on the right side of the creation dialog under "Your summary". Copy both before closing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the authentication level.&lt;/strong&gt; In the Settings panel on the left, find &lt;strong&gt;Minimal Authentication Level&lt;/strong&gt; and set it to &lt;strong&gt;Application&lt;/strong&gt;. This ensures only signed requests (like the ones in this post) are accepted. The default is Public, which allows unsigned requests and is intended for mobile clients calling Sinch directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: on a trial account you can only verify numbers on your verified numbers list. To verify any number, &lt;a href="https://community.sinch.com/t5/Customer-Dashboard/Upgrading-accounts/ta-p/7187" rel="noopener noreferrer"&gt;upgrade your account&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How verification works
&lt;/h2&gt;

&lt;p&gt;The flow is two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start&lt;/strong&gt;: your backend calls the Verification API with the phone number and method. Sinch delivers the code to the device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report&lt;/strong&gt;: the user enters the code. Your backend calls the API with the phone number and code. Sinch returns &lt;code&gt;SUCCESSFUL&lt;/code&gt; or &lt;code&gt;FAILED&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For SMS and voice, the code is a numeric OTP. For FlashCall, the "code" is the full incoming caller ID from the missed call. For Data Verification there is no report step: Sinch authenticates the device silently through the carrier's network and returns the result in the start response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SDK approach (Node.js)
&lt;/h2&gt;

&lt;p&gt;The Node.js SDK handles authentication internally. You initialize it once with your credentials. The Verification API has method-specific calls for start and report rather than a single generic endpoint, so each method gets its own SDK call.&lt;/p&gt;

&lt;p&gt;Note: the Python SDK doesn't include Verification yet. Python developers can use the raw HTTP section below.&lt;/p&gt;

&lt;p&gt;Install the SDK:&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; @sinch/sdk-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SinchClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sinch/sdk-core&lt;/span&gt;&lt;span class="dl"&gt;"&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;sinchClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SinchClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;applicationKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SINCH_APP_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;applicationSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SINCH_APP_SECRET&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;E164_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\+[&lt;/span&gt;&lt;span class="sr"&gt;1-9&lt;/span&gt;&lt;span class="se"&gt;]\d{1,14}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flashcall&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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;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;E164_REGEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid phone number format. Use E.164 (e.g. +15551234567), got: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;phoneNumber&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSms&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;startVerificationWithSmsRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&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="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="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flashcall&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="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startFlashCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;startVerificationWithFlashCallRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&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="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="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callout&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="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startPhoneCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;startVerificationWithPhoneCallRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startData&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;startDataVerificationRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reportVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flashcall&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reportSmsByIdentity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reportSmsVerificationByIdentityRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;});&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="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flashcall&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="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reportFlashCallByIdentity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reportFlashCallVerificationByIdentityRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flashCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cli&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="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="nx"&gt;sinchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reportPhoneCallByIdentity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reportPhoneCallVerificationByIdentityRequestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phoneCall&lt;/span&gt;&lt;span class="p"&gt;:&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="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;Call &lt;code&gt;startVerification&lt;/code&gt; when the user submits their phone number, and &lt;code&gt;reportVerification&lt;/code&gt; when they submit the code:&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="c1"&gt;// Step 1: user submits phone number&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+15559876543&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;started&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="c1"&gt;// Step 2: user submits the code from their device&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reportVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+15559876543&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123456&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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;SUCCESSFUL&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="c1"&gt;// issue session token, complete login, approve account change, etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful start response:&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;"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;"01ABC123DEF456GHI789JKL012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sms"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your verification code is {{CODE}}. Verified by Sinch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"interceptionTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;198&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;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;The response includes additional fields (&lt;code&gt;_links&lt;/code&gt;, etc.) that you don't need for the basic flow.&lt;/p&gt;

&lt;p&gt;A successful report response:&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;"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;"01ABC123DEF456GHI789JKL012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sms"&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;"SUCCESSFUL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manual"&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;If the code is wrong or expired, the SDK throws. Treat that as a prompt for the user to request a new code, not a retry of the same one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raw HTTP
&lt;/h2&gt;

&lt;p&gt;If you're using Python, or prefer no runtime dependency in Node.js, the same flow works with plain HTTP. Each request must be signed with HMAC-SHA256 using your App Secret. The signature covers the HTTP method, a hash of the request body, the content type, a timestamp, and the request path. This lets the server verify the request hasn't been tampered with and isn't a replay.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;hmac_mod&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;APP_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SINCH_APP_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;APP_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SINCH_APP_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;CONTENT_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json; charset=UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;E164_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\+[1-9]\d{1,14}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;content_md5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;string_to_sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&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;content_md5&lt;/span&gt;&lt;span class="si"&gt;}&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;CONTENT_TYPE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;x-timestamp:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&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;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hmac_mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&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;Application &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;APP_KEY&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;signature&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sinch_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http_method&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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CONTENT_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&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;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;http_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&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;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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;Sinch API error &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&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="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_verification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;E164_REGEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;Invalid phone number format. Use E.164 (e.g. +15551234567), got: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sinch_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://verification.api.sinch.com/verification/v1/verifications&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;POST&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;number&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;endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_verification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&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;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flashcall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flashcall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;callout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;callout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sinch_request&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;https://verification.api.sinch.com/verification/v1/verifications/number/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="si"&gt;}&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;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&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;h3&gt;
  
  
  Node.js (no SDK)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&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;APP_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;SINCH_APP_KEY&lt;/span&gt;&lt;span class="o"&gt;!&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;APP_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;SINCH_APP_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&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;CONTENT_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json; charset=UTF-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;contentMd5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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="dl"&gt;""&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;stringToSign&lt;/span&gt; &lt;span class="o"&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;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentMd5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CONTENT_TYPE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nx-timestamp:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&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;span class="nx"&gt;APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stringToSign&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&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;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Application &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;APP_KEY&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;signature&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;timestamp&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sinchRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&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;bodyStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bodyStr&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CONTENT_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-timestamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;bodyStr&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;bodyStr&lt;/span&gt; &lt;span class="p"&gt;}),&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Sinch API error &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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="nf"&gt;sinchRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://verification.api.sinch.com/verification/v1/verifications&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;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;method&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reportVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;method&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="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sms&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flashcall&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flashcall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cli&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callout&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sinchRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`https://verification.api.sinch.com/verification/v1/verifications/number/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;phoneNumber&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;body&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;Both functions raise on a non-2xx response. A wrong or expired code returns HTTP 400. Catch that and prompt the user to request a new code.&lt;/p&gt;

&lt;p&gt;Sinch also publishes SDKs for &lt;a href="https://developers.sinch.com/docs/sdks/java/" rel="noopener noreferrer"&gt;Java&lt;/a&gt; and &lt;a href="https://developers.sinch.com/docs/sdks/dotnet/" rel="noopener noreferrer"&gt;.NET&lt;/a&gt; if those are your languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things worth knowing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Verifications expire
&lt;/h3&gt;

&lt;p&gt;A started verification is only valid for a short window: a few minutes for SMS. If the user takes too long, the report call will fail. Build your UI to handle this: show a "resend code" option and treat expiry errors as a prompt to start fresh, not a permanent failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phone numbers must be E.164
&lt;/h3&gt;

&lt;p&gt;The phone number must be in E.164 format: &lt;code&gt;+15551234567&lt;/code&gt;, not &lt;code&gt;5551234567&lt;/code&gt; or &lt;code&gt;(555) 123-4567&lt;/code&gt;. Both the SDK and raw HTTP examples validate this before calling the API. The Sinch Verification API won't normalize it for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  FlashCall code format
&lt;/h3&gt;

&lt;p&gt;For FlashCall, the &lt;code&gt;code&lt;/code&gt; you pass to &lt;code&gt;reportVerification&lt;/code&gt; is the full incoming caller ID: the complete phone number that called the device. Your mobile client captures this from the call log and passes it to your backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Verification requires cellular data
&lt;/h3&gt;

&lt;p&gt;Data Verification only works when the device is on cellular data. If the user is on Wi-Fi, the carrier can't authenticate the device. Configure a fallback chain: attempt data first, then fall back to FlashCall or SMS. The API supports this natively so you don't have to retry manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Two functions, a clear two-step flow, and support for four verification methods. The SDK version needs no auth code. Initialize once and call. The raw HTTP version adds no dependencies at all. Both drop into any backend regardless of framework or platform.&lt;/p&gt;

&lt;p&gt;If you're building a mobile app, Sinch also publishes native SDKs for &lt;a href="https://developers.sinch.com/docs/verification/sdks/ios/" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; and &lt;a href="https://developers.sinch.com/docs/verification/sdks/android/" rel="noopener noreferrer"&gt;Android&lt;/a&gt;. They handle the client side of verification: automatically capturing the incoming caller ID for FlashCall so the user never has to read it, and managing the carrier network handshake for Data Verification. The backend code in this post pairs directly with those SDKs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/introduction" rel="noopener noreferrer"&gt;Sinch Verification API introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/api-reference/" rel="noopener noreferrer"&gt;Verification API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/sdks/ios/" rel="noopener noreferrer"&gt;Sinch iOS Verification SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/verification/sdks/android/" rel="noopener noreferrer"&gt;Sinch Android Verification SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/sdks/node/" rel="noopener noreferrer"&gt;Sinch Node.js SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/sdks/java/" rel="noopener noreferrer"&gt;Sinch Java SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.sinch.com/docs/sdks/dotnet/" rel="noopener noreferrer"&gt;Sinch .NET SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.sinch.com/t5/Customer-Dashboard/How-to-sign-up-for-your-free-Sinch-account/ta-p/8058" rel="noopener noreferrer"&gt;Sign up for a Sinch account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.sinch.com/t5/Customer-Dashboard/Upgrading-accounts/ta-p/7187" rel="noopener noreferrer"&gt;Upgrading your Sinch account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What are you building that needs user verification? Let me know in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>typescript</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
