Every handler receives a FunctionContext as its first argument (Node) or as an injected dependency (C#). Think of it as the standard library of the Sinch runtime — everything you need to interact with state, configuration, and the rest of Sinch is hanging off of it.
Both runtimes expose the same capabilities with idiomatic names.
| Capability | Node.js | C# |
|---|---|---|
| Key-value cache | context.cache | Context.Cache |
| Blob / file storage | context.storage | Context.Storage |
| SQLite database | context.database (path) | Context.Database (conn) |
| Structured logger | context.log / console | Context.Logger |
| Project + function info | context.config | Context.Configuration |
| Env vars | context.env | Context.Configuration |
| Request metadata | context.requestId, .timestamp | Available via HttpContext |
| Pre-wired Voice SDK | context.voice | Context.Voice |
| Pre-wired Conversation SDK | context.conversation | Context.Conversation |
| Pre-wired SMS SDK | context.sms | Context.Sms |
| Pre-wired Numbers SDK | context.numbers | Context.Numbers |
| Pre-wired Verification SDK | — | Context.Verification |
| Read private assets | context.assets(filename) | Package-level helpers |
The SDK client properties are null / undefined if the corresponding environment variables are not set — see configuration & secrets for which vars unlock which client.
Key-value store with TTL. Use it for per-call state (keyed by callid), rate limiting, session data, and anything you want to survive between callbacks without a full database trip.
- Dev: in-memory. Lost on restart.
- Prod: persistent, shared across invocations, distributed.
// Node
await context.cache.set('session:abc', { userId: 'u1' }, 1800);
const session = await context.cache.get<{ userId: string }>('session:abc');
await context.cache.extend('session:abc', 600);
await context.cache.delete('session:abc');
const keys = await context.cache.keys('session:*');// C#
await Context.Cache.Set("session:abc", new { UserId = "u1" }, 1800);
var session = await Context.Cache.Get<MySession>("session:abc");
if (await Context.Cache.Exists("session:abc"))
await Context.Cache.Delete("session:abc");Default TTL is 3600 seconds. Values are JSON-serialized.
Blob storage for persistent files — call recordings, reports, user uploads, arbitrary data.
- Dev: local filesystem in
./storage/. - Prod: durable cloud storage with a local read cache.
// Node
await context.storage.write('reports/daily.json', JSON.stringify(data));
const buf = await context.storage.read('reports/daily.json');
const files = await context.storage.list('reports/');
const exists = await context.storage.exists('reports/old.json');
await context.storage.delete('reports/old.json');// C#
await Context.Storage.WriteAsync("reports/daily.json", JsonSerializer.Serialize(data));
var text = await Context.Storage.ReadTextAsync("reports/daily.json");
var files = await Context.Storage.ListAsync("reports/");
// Streaming for big files
using var stream = await Context.Storage.ReadStreamAsync("large-file.bin");Keys can include path separators — treat them like folder paths.
A per-function SQLite database. You bring your own SQLite library; the runtime manages the file location and replication.
- Dev: plain local SQLite file in
./data/. - Prod: SQLite with automatic durable replication. The database survives restarts and scale events, with no code changes required.
Pure JavaScript SQLite compiled to WebAssembly. No native compilation.
import initSqlJs from 'sql.js';
import { readFileSync, writeFileSync, existsSync } from 'fs';
const SQL = await initSqlJs();
const buf = existsSync(context.database) ? readFileSync(context.database) : undefined;
const db = new SQL.Database(buf);
db.run('CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)');
db.run('INSERT OR REPLACE INTO kv VALUES (?, ?)', ['greeting', 'hello']);
writeFileSync(context.database, Buffer.from(db.export()));
db.close();better-sqlite3 is faster but needs native compilation — use it when you can guarantee the build environment has python3 / make / g++.
using var conn = new SqliteConnection(Context.Database.ConnectionString);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "CREATE TABLE IF NOT EXISTS call_log (id INTEGER PRIMARY KEY, caller TEXT, ts INTEGER)";
cmd.ExecuteNonQuery();Or with Dapper:
using var conn = new SqliteConnection(Context.Database.ConnectionString);
var calls = await conn.QueryAsync<CallRecord>("SELECT * FROM call_log ORDER BY ts DESC LIMIT 10");context.config exposes project and function metadata — useful for logging, feature flags by environment, and constructing return URLs.
context.config.projectId // your Sinch Project ID
context.config.functionName // e.g. 'my-ivr'
context.config.environment // 'development' | 'production'
context.config.variables // object of sinch.json variablesFor a richer API, use createConfig(context) (aka createUniversalConfig):
import { createConfig } from '@sinch/functions-runtime';
const config = createConfig(context);
const apiKey = config.requireSecret('STRIPE_SECRET_KEY'); // throws if missing
const company = config.getVariable('COMPANY_NAME', 'Acme Corp');
if (config.isProduction()) { /* ... */ }In C#, use the injected IConfiguration:
var companyName = Configuration["COMPANY_NAME"] ?? "Acme Corp";
var apiKey = Configuration["STRIPE_SECRET_KEY"]
?? throw new InvalidOperationException("STRIPE_SECRET_KEY is required");The runtime instantiates Sinch SDK clients for you and attaches them to the context. No credential management in your function.
| Client | Node | C# | Env vars required |
|---|---|---|---|
| Voice | context.voice | Context.Voice | VOICE_APPLICATION_KEY, VOICE_APPLICATION_SECRET |
| Conversation | context.conversation | Context.Conversation | CONVERSATION_APP_ID |
| SMS | context.sms | Context.Sms | SMS_SERVICE_PLAN_ID |
| Numbers | context.numbers | Context.Numbers | ENABLE_NUMBERS_API=true |
| Verification (C#) | — | Context.Verification | VERIFICATION_APPLICATION_ID, VERIFICATION_APPLICATION_SECRET |
All clients require the base Project credentials: PROJECT_ID, PROJECT_ID_API_KEY, PROJECT_ID_API_SECRET.
If a required variable is missing, the property is null — always null-check before using:
if (context.conversation) {
await context.conversation.messages.send({ ... });
}if (Context.Conversation is not null)
await Context.Conversation.Messages.Send(...);Private files you ship alongside your code — prompts, JSON data, templates. Keep them in the assets/ directory and read them with context.assets(filename). Do not use fs.readFileSync('./assets/...') — the deployed artifact structure is different from your dev tree, and the runtime handles the translation for you.
const greetingText = await context.assets('greetings/en.txt');For public static files (images, CSS, public JSON), use the public/ directory instead. The runtime serves it at /.
context.log (Node) and Context.Logger (C#) are structured loggers. In production, output is captured, indexed, and streamed via sinch functions logs.
context.log?.info('Call received', { callId: data.callid, cli: data.cli });Logger.LogInformation("Call received from {Cli}", data.Cli);In Node, console.log also works — it's captured the same way as context.log.
context.requestId— a tracing ID unique to this invocation. Include it in logs and when calling out to other services; it makes cross-service debugging bearable.context.timestamp— ISO 8601 timestamp when the request entered the runtime.
- Configuration & secrets — how env vars,
sinch.jsonvariables, and secrets flow - Local vs prod — what changes between dev and deployed environments
- Function context reference — Node + C# side-by-side cheat sheet