Sinch.Functions.Runtime
0.3.20
See the version list below for details.
dotnet add package Sinch.Functions.Runtime --version 0.3.20
NuGet\Install-Package Sinch.Functions.Runtime -Version 0.3.20
<PackageReference Include="Sinch.Functions.Runtime" Version="0.3.20" />
<PackageVersion Include="Sinch.Functions.Runtime" Version="0.3.20" />
<PackageReference Include="Sinch.Functions.Runtime" />
paket add Sinch.Functions.Runtime --version 0.3.20
#r "nuget: Sinch.Functions.Runtime, 0.3.20"
#:package Sinch.Functions.Runtime@0.3.20
#addin nuget:?package=Sinch.Functions.Runtime&version=0.3.20
#tool nuget:?package=Sinch.Functions.Runtime&version=0.3.20
Sinch.Functions.Runtime
Development runtime for Sinch Functions - build serverless communication workflows with ease.
Getting Started
The easiest way to get started is using the Sinch CLI:
# Install the CLI
npm install -g @sinch/cli
# Create a new function (interactive)
sinch functions init
The interactive prompt will guide you through:
- Selecting a runtime (Node.js, C#, etc.)
- Choosing a template (simple-voice-ivr recommended for first project)
- Setting up your project
Then start the local development server:
sinch functions dev
Your function is now running locally with hot reload!
Deploy to Production
When you're ready to deploy:
sinch functions deploy
Your function will be built and deployed to the Sinch Functions platform.
Features
Voice API
- SinchVoiceController - Base controller for voice callbacks (ICE, PIE, ACE, DICE)
- SVAML Builders - Fluent API for building voice responses (
IceSvamletBuilder,PieSvamletBuilder) - Menu Utilities - IVR menu builders (
MenuFactory.CreateMenu()) - Voice Helpers - TTS, audio playback, and call control utilities
- Webhook Validation - HMAC signature validation for Voice API callbacks
- Realtime Voice Agent -
RealtimeAgent/AddRealtimeAgentstays on the line for the whole call, relaying audio to a vendor's realtime voice API and dispatching its tool calls in-process (Deepgram, OpenAI GPT-Live, xAI Grok, Google Gemini)
Conversation API
- SinchConversationController - Base controller for omnichannel messaging (SMS, WhatsApp, Messenger, Viber)
- Message Builders - Static helpers and fluent builder for messages
- Extension Methods - Easy access to inbound message properties
- Multi-Channel Support - Automatic SMS_SENDER handling, channel-specific configurations
- Webhook Routing - Automatic routing to typed handlers
Infrastructure
- FunctionContext - Pre-configured SDK clients, caching, logging
- Local Development - In-memory caching, tunnel support for local debugging
- SDK Integration - Pre-configured Sinch SDK clients for Voice and Conversation APIs
Voice IVR Example
using SinchFunctions.Utils;
public class FunctionController : SinchVoiceController
{
public FunctionController(FunctionContext context, IConfiguration configuration, ILogger<FunctionController> logger)
: base(context, configuration, logger) { }
public override async Task<IActionResult> Ice([FromBody] IceCallbackModel callbackData)
{
// Create a voice menu
var menu = MenuFactory.CreateMenu()
.Prompt("Welcome to Sinch! Press 1 for sales, 2 for support.")
.Option("1", "return(sales)")
.Option("2", "return(support)")
.Build();
// Return SVAML response
return Ok(new IceSvamletBuilder()
.Action.RunMenu(menu)
.Build());
}
public override async Task<IActionResult> Pie([FromBody] PieCallbackModel callbackData)
{
var selection = callbackData.MenuResult?.Value;
return selection switch
{
"sales" => Ok(new PieSvamletBuilder().Action.ConnectPstn("+15551234567").Build()),
"support" => Ok(new PieSvamletBuilder().Action.ConnectPstn("+15559876543").Build()),
_ => Ok(new PieSvamletBuilder().Action.Hangup().Build())
};
}
}
Realtime Voice Agent Example
ConnectAgent hands the call away — the vendor's own agent runtime takes it from there.
RealtimeAgent is the opposite: this function stays on the line for the whole call, relaying
audio to a vendor's realtime voice API and dispatching its tool calls in-process. Every adapter is
System.Net.WebSockets.ClientWebSocket — no vendor SDK.
Use*() |
Vendor | Notes |
|---|---|---|
UseDeepgram(configure) |
Deepgram Voice Agent API | No resampling — Deepgram accepts the platform's own negotiated rate. Every declared tool defaults to defer_until_eot: true. Barge-in is gated on an estimated playout window rather than raised on every UserStartedSpeaking, which Deepgram's own docs say fires for every utterance, not only a genuine interruption. Listen defaults to flux-general-en (Flux, on Deepgram's /v2/listen); a nova-* model omits the version field, a flux* one sends "version": "v2" alongside it — a Flux model without it falls back to the Nova endpoint, where it's invalid. ThinkProvider (default open_ai) picks the LLM vendor behind Think: open_ai, anthropic, google, groq, or aws_bedrock. |
UseOpenAi() |
OpenAI GPT-Live (gpt-live-1) — not the Realtime API |
Responses delegation, 8kHz audio/pcmu both ways. Sends audio continuously — a caller leg that goes quiet still needs frames flowing, since GPT-Live paces entirely off inbound audio and a gap in it reads as the leg having stalled, not as silence. No barge-in signal exists; the agent-turn boundary is a silence gap on output audio (GPT-Live streams silence padding continuously between utterances, so only audio whose decoded RMS clears roughly -50 dBFS counts as speech and resets the gap timer — otherwise the gap would never elapse), never on caller speech, with a 30s hard-cap flush and a short fallback window for a transcript delta that arrives just after the audio-side flush already ran. |
UseGrok() |
xAI Grok | Realtime-API-shaped (flat session.update), 8kHz audio/pcmu both ways. greeting is paraphrased, not verbatim — sent as response.create instructions, not a dedicated field. |
UseGemini() |
Google Gemini Live | Fixed 16kHz in / 24kHz out, resampled through a stateful anti-aliasing resampler. Reconnects transparently (capped retries) on a session-window close, using Gemini's own resumption handle. The caller audio stream must stay continuous — SendAudio forwards every frame the Sinch leg sends unconditionally, including silence, since a gap in it is what earlier looked like a vendor-side silence bug and turned out to be a probe pausing caller audio, not anything Gemini itself did. |
UseVoiceRelay(configure) |
Any OpenAI Responses-compatible endpoint | No audio at all — the agent's leg is a relay leg (platform STT/TTS), not a stream leg, and the LLM loop runs in text against POST {BaseUrl}/responses (stream: true), never Chat Completions. VoiceRelayAgentOptions.Model is required and passed through verbatim, no provider prefix; BaseUrl defaults to https://api.openai.com/v1, overridable by the OPENAI_BASE_URL configuration value or the option itself. Works with any model behind an OpenAI Responses-compatible endpoint — point BaseUrl/OPENAI_BASE_URL elsewhere and use whatever model name that server supports. Needs OPENAI_API_KEY as a Bearer token to whichever server. Conversation state is previous_response_id, not the full input list. A caller barge-in (relay interrupt) cancels whatever response is in flight. The same RealtimeAgent subclass works unchanged: Prompt, Greeting, [AgentTool]s, EscalationNumber, the built-in hangup_call/transfer_to_human, Call.Transcript/OnTranscript, OnEndAsync, MaxCallDuration — only Voice/ResolveVoiceAsync don't apply (the relay leg's own TTS voice is VoiceRelayAgentOptions.Voice). |
/stream (/relay for UseVoiceRelay) requires a one-time token Connect() mints per call — nothing an author does, but worth
knowing when reading logs: a connection that never answers because a token was unknown, expired,
or already consumed reached /stream without going through this agent's own call.incoming plan.
The platform is also told answer only once the vendor session actually acks ready (Deepgram
SettingsApplied, GPT-Live session.started, Grok session.updated, Gemini setupComplete) — a
vendor outage or a bad API key rejects the call outright instead of answering into silence.
using SinchFunctions.AI.Realtime;
public class ReservationsAgent : RealtimeAgent
{
protected override string Prompt =>
"You are a reservations agent for Acme Cafe. Be brief and friendly.";
protected override string? Greeting => "Hello! Thanks for calling Acme Cafe.";
// protected override string? Voice => "aura-2-thalia-en"; // vendor specific, omit for the vendor default
// A CancellationToken parameter is injected, not read from the vendor's JSON arguments -- it's
// cancelled if the vendor tells this call it no longer wants the result (e.g. Deepgram's
// FunctionCallCancelled, Gemini's toolCallCancellation) or the call itself ends.
[AgentTool("Check table availability for a given date and party size.")]
public async Task<object> CheckAvailability(string date, int partySize, CancellationToken cancellationToken)
{
var slots = await LookUpAvailabilityAsync(date, partySize, cancellationToken); // ... your own lookup ...
return new { available = slots.Count > 0, times = slots };
}
[AgentTool("Book a table once the caller has confirmed a date, time and party size.")]
public async Task<object> BookTable(string date, string time, int partySize, string name)
{
var confirmationId = $"BK-{DateTimeOffset.UtcNow.Ticks:x}";
await Call.Context.Storage.WriteAsync($"bookings/{confirmationId}.json", /* ... */ "{}");
return new { confirmationId, message = $"Booked for {name}, {partySize} guests, {date} {time}." };
}
[AgentTool("Transfer the call to the host stand for anything this agent cannot resolve.")]
public async Task<object> TransferToHost(string reason)
{
await Call.TransferToPhoneAsync("+15550001234");
return new { transferred = true, reason };
}
// Offers the built-in transfer_to_human tool too, targeting this number; leave unset (the
// default) and it is not offered at all.
protected override string? EscalationNumber => "+15550001234";
protected override void OnTranscript(RealtimeTranscriptEntry entry) =>
Console.WriteLine($"[{Call.Id}] {entry.Role}: {entry.Text}");
protected override Task OnEndAsync(RealtimeCallSummary summary) => Task.CompletedTask;
}
// Program.cs / ConfigureServices
services.AddRealtimeAgent<ReservationsAgent>(agent => agent.UseDeepgram());
public class FunctionController : SinchVoiceController
{
private readonly IRealtimeAgent<ReservationsAgent> _agent;
public FunctionController(
FunctionContext context, IConfiguration configuration, ILogger<FunctionController> logger,
IRealtimeAgent<ReservationsAgent> agent)
: base(context, configuration, logger) => _agent = agent;
protected override CallFlow OnIncoming(WebhookRequest request) => _agent.Connect(request);
}
Per-call overrides. Prompt and Greeting are plain overridable properties, evaluated fresh
for each call since a RealtimeAgent instance is scoped per call. Voice is a plain nullable
string for a fixed choice; override ResolveVoiceAsync() instead (or its cancellation-aware
overload ResolveVoiceAsync(CancellationToken)) when the choice depends on the call, e.g. picked
from Call.Id or a stored preference. The default ResolveVoiceAsync() just forwards to Voice,
resolved once, right before the vendor session opens.
Voice. Unset means no voice is sent to the vendor at all — every adapter omits the field and lets the vendor apply its own default, rather than this package picking one.
Tool cancellation. A tool handler that declares a CancellationToken parameter (see
CheckAvailability above) gets one that's actually cancelled when the vendor says it no longer
wants this call's result (Deepgram's FunctionCallCancelled, Gemini's toolCallCancellation) or
the call itself ends -- not just a bookkeeping entry cleared while the handler keeps running to
completion regardless. Grok and GPT-Live don't have a vendor-initiated cancellation frame of their
own, so the token there only ever fires when the call ends. The parameter is optional and, if
present, is never bound from the vendor's JSON arguments.
What's normalised: the tool contract ([AgentTool]), the lifecycle hooks
(OnCallStartingAsync/OnTranscript/OnBargeIn/OnError/OnEndAsync), call control from
a tool handler (Call.Hangup(), Call.TransferToPhoneAsync(number) and its TransferToSip/
TransferToStream/TransferToRelay/TransferToAgent siblings — see TransferToHost above),
audio format, and prompt/greeting delivery. What isn't: voice ids, a vendor's own split
STT/LLM/TTS model choices, and turn-detection/latency feel — those are vendor-specific
configuration on each Use*() call (e.g. UseDeepgram(o => o.Think = "gpt-5.4-nano")). Audio
defaults to 8kHz telephony, matching the Voice v2 platform's own default for calls that traverse
the PSTN; set agent.Audio = RealtimeAudioRate.Wideband for a media path that never does. The
stream leg is capped at the MAX_CALL_DURATION configuration value (seconds), falling back to
1800.
Built-in tools. Every agent is offered hangup_call (always) and transfer_to_human (only
when EscalationNumber is set). An author's own [AgentTool] method of the same name replaces
the built-in rather than running alongside it.
The transcript. Call.Transcript and RealtimeCallSummary.Transcript carry every caller turn,
agent turn and completed tool call, in order, as an IReadOnlyList<RealtimeTranscriptEntry>.
OnTranscript(RealtimeTranscriptEntry entry) fires once per entry as it's added.
Secrets (DEEPGRAM_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GOOGLE_API_KEY) are read from
IConfiguration, the same way every other function reads a secret.
Conversation API Chatbot Example
using Sinch.Conversation.Hooks;
using SinchFunctions.Utils;
public class FunctionController : SinchConversationController
{
public FunctionController(FunctionContext context, IConfiguration config, ILogger<FunctionController> logger)
: base(context, config, logger) { }
public override async Task<IActionResult> MessageInbound(MessageInboundEvent inbound)
{
// Use extension methods for easy property access
var text = inbound.GetText();
var channel = inbound.GetChannel();
var contactId = inbound.GetContactId();
if (string.IsNullOrEmpty(text))
{
Logger.LogDebug("Ignoring non-text message from {ContactId}", contactId);
return Ok();
}
Logger.LogInformation("Message from {ContactId} via {Channel}: {Text}", contactId, channel, text);
// Reply using helper method (auto-sets SMS_SENDER from config for SMS)
await Context.Conversation!.Messages.Send(Reply(inbound, "Thanks for your message!"));
return Ok(new { status = "ok" });
}
public override Task<IActionResult> MessageDelivery(MessageDeliveryReceiptEvent callback)
{
var status = callback.MessageDeliveryReport?.Status?.Value;
Logger.LogInformation("Delivery status: {Status}", status);
return Task.FromResult<IActionResult>(Ok());
}
}
Conversation API Extension Methods
// Easy access to inbound message properties
var text = inbound.GetText(); // Message text content
var channel = inbound.GetChannel(); // SMS, WHATSAPP, MESSENGER, etc.
var identity = inbound.GetIdentity(); // Sender's channel identity (phone/PSID)
var contactId = inbound.GetContactId(); // Sinch contact ID
var to = inbound.GetTo(); // Number the message was sent TO
// Check message types
if (inbound.IsTextMessage()) { /* handle text */ }
if (inbound.IsMediaMessage()) { /* handle media */ }
if (inbound.IsPostback()) { /* handle button click */ }
Conversation API Message Builders
// Simple reply - uses channel from inbound message
await Context.Conversation!.Messages.Send(Reply(inbound, "Hello!"));
// Static helper for SMS
var msg = ConversationMessage.CreateSms(appId, "+15551234567", "Hello!", smsSender);
await Context.Conversation!.Messages.Send(msg);
// Static helper for WhatsApp
var msg = ConversationMessage.CreateWhatsApp(appId, "+15551234567", "Hello!");
await Context.Conversation!.Messages.Send(msg);
// Fluent builder for complex cases
var msg = CreateMessage(inbound)
.Text("Custom reply")
.Build();
await Context.Conversation!.Messages.Send(msg);
Conversation API Webhook Events
| Trigger | Method | Description |
|---|---|---|
| MESSAGE_INBOUND | MessageInbound() |
User sends a message |
| MESSAGE_DELIVERY | MessageDelivery() |
Delivery status update |
| EVENT_INBOUND | EventInbound() |
Typing indicators, read receipts |
| CONVERSATION_START | ConversationStart() |
Conversation begins |
| CONVERSATION_STOP | ConversationStop() |
Conversation ends |
Conversation API Configuration
| Variable | Description | Required |
|---|---|---|
CONVERSATION_APP_ID |
Your Conversation API app ID | Yes |
FROM_NUMBER |
Your SMS sender number (E.164) | For SMS only |
CONVERSATION_REGION |
API region (US, EU, BR) | No (default: US) |
Request Limits
| Limit | Value |
|---|---|
| Request body | 10 MB (10485760 bytes) |
| Request/response body retained in logs | First 64 KB |
A body larger than the limit is rejected with 413 before your handler runs.
The platform router in front of a deployed function enforces the same 10 MB cap, so a body that
your handler accepts locally is not rejected in transit once deployed. Exactly 10485760 bytes is
accepted; one byte more returns 413.
Logs retain the first 64 KB of a request or response body, appending
…[truncated — N bytes total] when the body was longer. Only text formats are retained — JSON,
XML, plain text and form data. Anything else, including uploads, images, audio, video, PDFs and
archives, is not logged at all. Your function still receives every body in full.
Documentation
License
MIT License - see LICENSE file for details
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- libphonenumber-csharp (>= 9.0.40)
- Sinch (>= 1.5.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.3.22 | 97 | 9/29/2026 |
| 0.3.21 | 102 | 9/27/2026 |
| 0.3.20 | 106 | 9/26/2026 |
| 0.3.19 | 111 | 9/26/2026 |
| 0.3.18 | 108 | 9/25/2026 |
| 0.3.17 | 112 | 9/23/2026 |
| 0.3.16 | 113 | 9/22/2026 |
| 0.3.14 | 125 | 9/18/2026 |
| 0.3.13 | 134 | 9/8/2026 |
| 0.3.12 | 132 | 9/2/2026 |
| 0.3.11 | 120 | 8/24/2026 |
| 0.3.10 | 126 | 8/14/2026 |
| 0.3.9 | 119 | 8/7/2026 |
| 0.3.8 | 122 | 8/5/2026 |
| 0.3.7 | 171 | 5/12/2026 |
| 0.3.6 | 139 | 4/28/2026 |
| 0.3.5 | 130 | 4/28/2026 |
| 0.3.4 | 202 | 3/17/2026 |
| 0.3.3 | 154 | 3/12/2026 |
| 0.3.2 | 140 | 3/11/2026 |
See Sinch.Functions.Runtime for release notes