# Node.js runtime

**It's Express with conventions.** You export handlers, the runtime routes URLs to them, you receive a request and return a response. If you already know Express, skim this page and you are basically done.

Package: `@sinch/functions-runtime`

## Project structure

A typical Node function looks like this:

```
my-function/
├── function.ts        ← entry point — all exports live here
├── package.json
├── tsconfig.json      ← uses the shared config, do not change module settings
├── sinch.json         ← project manifest (name, runtime, variables, secrets)
├── .env               ← local dev secrets (gitignored)
├── assets/            ← private files, read with context.assets()
└── public/            ← static files, served at /
```

All source files live at the project root — the runtime expects them flat. You can split logic into `harness.ts`, `db.ts`, `voice.ts`, etc., and import them from `function.ts`. Relative imports use `.js` extensions because the shared tsconfig uses NodeNext:

```typescript
import { handleIce } from './voice.js';
import { handleMessage } from './harness.js';
```

## Hello world

```typescript
// function.ts
import type { VoiceFunction } from '@sinch/functions-runtime';
import { createIceBuilder } from '@sinch/functions-runtime';

export default {
  async ice(context, data) {
    return createIceBuilder()
      .say('Hello from Sinch Functions!')
      .hangup()
      .build();
  },
} satisfies VoiceFunction;
```

That's a complete, deployable voice function. Save, run `sinch functions dev`, point a Sinch number at the tunnel URL, and call it.

## Handlers: the Express part

Every named property on the default export becomes an HTTP endpoint. The runtime matches request paths to export names:

| Request path | Export called | Treated as |
|  --- | --- | --- |
| `POST /ice` | `ice` | Voice callback |
| `POST /ace` | `ace` | Voice callback |
| `POST /pie` | `pie` | Voice callback |
| `POST /dice` | `dice` | Voice callback |
| `POST /webhook/conversation` | `conversationWebhook` | Conversation |
| `GET /api/health` | `health` | Custom HTTP |
| `POST /api/v2/users` | `users` | Custom HTTP |
| `GET /` | `default` or `home` | Custom HTTP root |


See [handlers](/docs/functions/functions/concepts/handlers) for the full rules. Short version: the **last path segment** is matched to an export name, `ice`/`ace`/`pie`/`dice`/`notify` are always voice callbacks, and `/webhook/<service>` maps to `<service>Webhook` camelCased.

## The context argument

Every handler receives a `FunctionContext` as its first argument. It gives you cache, storage, database, a logger, and pre-wired SDK clients:

```typescript
async ice(context, data) {
  await context.cache.set(`call:${data.callid}`, { cli: data.cli }, 3600);
  const greeting = await context.assets('greetings/en.txt');

  return createIceBuilder()
    .say(greeting)
    .runMenu(MenuTemplates.business(context.config.variables.COMPANY_NAME))
    .build();
}
```

See [context object](/docs/functions/functions/concepts/context-object) for the full tour, or [function context reference](/docs/functions/reference/function-context) for the Node + C# side-by-side cheat sheet.

## Custom HTTP endpoints

Any named export that is not a voice callback becomes a custom HTTP endpoint. Signature is `(context, request)` and you return `{ statusCode, body, headers? }`:

```typescript
import type { FunctionContext, FunctionRequest } from '@sinch/functions-runtime';

export async function status(context: FunctionContext, request: FunctionRequest) {
  return { statusCode: 200, body: { status: 'healthy', uptime: process.uptime() } };
}

export async function createUser(context: FunctionContext, request: FunctionRequest) {
  const body = request.body as { name?: string };
  if (!body.name) {
    return { statusCode: 400, body: { error: 'Missing name' } };
  }
  return { statusCode: 201, body: { id: 'u_123', name: body.name } };
}
```

`FunctionRequest` has `method`, `path`, `query`, `headers`, `body`, `params`. Return any `{ statusCode, body, headers? }` object.

See [add a custom endpoint](/docs/functions/functions/guides/add-a-custom-endpoint) for a fuller example.

## Responding to voice callbacks

Voice callbacks return SVAML via one of the three builders. Each callback type has its own builder with the right subset of actions:

```typescript
import { createIceBuilder, createAceBuilder, createPieBuilder } from '@sinch/functions-runtime';

// ICE — full action surface
return createIceBuilder()
  .say('Welcome!')
  .connectPstn('+15551234567', { cli: data.cli })
  .build();

// ACE — continue or hangup only
return createAceBuilder().continue().build();

// PIE — respond to a menu result
return createPieBuilder()
  .say('Connecting you to sales.')
  .connectPstn('+15551111111')
  .build();
```

See [SVAML cheat sheet](/docs/functions/reference/svaml-cheatsheet) for the most-used actions and [voice callbacks](/docs/functions/functions/concepts/voice-callbacks) for the lifecycle.

## Conversation webhooks

Two approaches:

**Simple:** export a `conversationWebhook` function and parse the body yourself.

**Structured:** extend `ConversationController` and override the event methods you care about.

```typescript
import type { FunctionContext } from '@sinch/functions-runtime';
import { ConversationController, getText, getChannel, getContactId } from '@sinch/functions-runtime';

class MyBot extends ConversationController {
  async handleMessageInbound(event) {
    const text = getText(event);
    const channel = getChannel(event);

    await this.conversation!.messages.send({
      sendMessageRequestBody: this.reply(event, `You said: ${text}`),
    });
  }
}
```

Helper functions for navigating inbound events without nested property access:

```typescript
import {
  getText, getMedia, getPostbackData, getContactId, getConversationId,
  getChannel, getIdentity, getTo, getLocation,
  isTextMessage, isMediaMessage, isPostback,
} from '@sinch/functions-runtime';
```

See [build an SMS responder](/docs/functions/functions/guides/build-an-sms-responder) for a worked example.

## Startup hook and WebSockets

Export a `setup()` function to run initialization before the server accepts requests, or to register WebSocket endpoints (e.g., for `connectStream` audio bridging).

```typescript
import type { SinchRuntime, FunctionContext } from '@sinch/functions-runtime';

export function setup(runtime: SinchRuntime) {
  runtime.onStartup(async (context: FunctionContext) => {
    // Create tables, warm caches, open connections
  });

  runtime.onWebSocket('/stream', (ws, req) => {
    ws.on('message', (data) => { /* audio frames */ });
  });
}
```

`SinchRuntime` is intentionally narrow. You cannot reach the Express app or add middleware — the runtime guarantees platform middleware runs regardless of what your function does.

## UniversalConfig

Convenience wrapper around `context.config` and secrets:

```typescript
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()) { /* prod only */ }
```

## Voice utilities

```typescript
import {
  toLocalPhoneNumber,    // '+15551234567' → '(555) 123-4567'
  formatDuration,        // 150 → '2m 30s'
  extractCallerNumber,   // strip non-digits
  VoiceErrorHelper,      // .serviceUnavailable(), .invalidInput(), .goodbye()
} from '@sinch/functions-runtime';
```

## Do and don't

- **Do** use the builders for SVAML — never return raw JSON from voice callbacks.
- **Do** use `context.assets(...)` for private files, not `fs.readFileSync`.
- **Do** use the `.js` extension in relative imports — the tsconfig is NodeNext.
- **Don't** add instructions after calling `.build()` — the builder is frozen at that point.
- **Don't** use `createAceBuilder` to connect calls — ACE only supports `continue` and `hangup`.
- **Don't** override `module` or `moduleResolution` in your `tsconfig.json` — the shared base handles it.


## Related

- [Handlers concept](/docs/functions/functions/concepts/handlers) — URL-to-handler mapping in detail
- [Context object concept](/docs/functions/functions/concepts/context-object) — cache/storage/database/SDK clients
- [Function context reference](/docs/functions/reference/function-context) — side-by-side cheat sheet
- [SVAML cheat sheet](/docs/functions/reference/svaml-cheatsheet) — most-used actions
- [C# runtime](/docs/functions/functions/runtimes/csharp) — the same thing for .NET devs