# Handlers

If you know Express or ASP.NET MVC, you already know 90% of the Sinch runtime. This page is the other 10% — the conventions Sinch uses to decide which of your handlers runs when a request comes in.

## The mental model

> **Node runtime is effectively Express with conventions.** You export handlers, the runtime maps URL paths to them, you get a request and return a response.
**C# runtime is ASP.NET MVC with controllers.** You extend a base controller, override the methods you care about, and dependency injection wires up services and SDK clients.


That's it. Everything below is the specifics.

## Node.js: named exports are routes

Your `function.ts` file exports an object (or individual named exports). Each export becomes an HTTP endpoint.

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

export default {
  // POST /ice — voice callback, auto-detected by name
  async ice(context, data) {
    return createIceBuilder().say('Welcome!').hangup().build();
  },

  // GET /status — custom HTTP endpoint
  async status(context, request) {
    return { statusCode: 200, body: { ok: true } };
  },

  // GET /health — custom HTTP endpoint
  async health(context, request) {
    return { statusCode: 200, body: { healthy: true } };
  },
} satisfies VoiceFunction;
```

### Routing rules

| 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 /notify` | `notify` | 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 |


The rules in plain English:

- **The last segment of the URL path is matched to an export name.** `/api/v2/users` → export `users`.
- **`ice`, `ace`, `pie`, `dice`, `notify` are voice callbacks regardless of path.** They get the callback payload as `data` and should return SVAML.
- **`/webhook/<service>` is a special pattern.** It maps to a camelCase name with `Webhook` suffixed — `/webhook/conversation` → `conversationWebhook`.
- **`default` and `home` both catch `GET /`.** Use whichever feels more natural; `home` is TypeScript-friendlier since `default` is a reserved word.


### Voice callbacks vs custom endpoints

| Handler type | Signature | Returns |
|  --- | --- | --- |
| Voice callback | `(context, data: Voice.IceRequest)` | SVAML (from a builder) |
| Custom endpoint | `(context, request: FunctionRequest)` | `{ statusCode, body }` |


Return any plain `{ statusCode, body, headers? }` object:

```typescript
return { statusCode: 200, body: { status: 'healthy' } };
return { statusCode: 400, body: { error: 'Missing required field: name' } };
return { statusCode: 404, body: { error: 'Not found' } };
```

## C#: extend a controller

```csharp
public class FunctionController : SinchVoiceController
{
    public FunctionController(
        FunctionContext context,
        IConfiguration configuration,
        ILogger<FunctionController> logger)
        : base(context, configuration, logger) { }

    public override async Task<IActionResult> Ice(IceCallbackModel data)
    {
        return Ok(new IceSvamletBuilder()
            .Instructions.Say("Welcome!")
            .Action.Hangup()
            .Build());
    }

    public override Task<IActionResult> Pie(PieCallbackModel data) => ...;
    public override Task<IActionResult> Ace(AceCallbackModel data) => ...;
    public override Task<IActionResult> Dice(DiceCallbackModel data) => ...;
}
```

### The four base controllers

| Base class | When to use |
|  --- | --- |
| `SinchVoiceController` | Voice callback handling. Override `Ice`, `Ace`, `Pie`, `Dice`. |
| `SinchConversationController` | Conversation API webhooks (SMS, WhatsApp, Messenger, etc.). |
| `ElevenLabsController` | ElevenLabs AI voice agent webhooks. |
| `SinchController` | Plain HTTP controllers (your own REST endpoints). |


You can have more than one controller in your project — extend `SinchVoiceController` for voice and `SinchController` for your custom API endpoints side by side.

### Custom HTTP endpoints

For non-voice endpoints, use a regular ASP.NET MVC controller that extends `SinchController`:

```csharp
[Route("api")]
public class MyController : SinchController
{
    public MyController(FunctionContext context, IConfiguration config, ILogger<MyController> logger)
        : base(context, config, logger) { }

    [HttpGet("status")]
    public IActionResult GetStatus() => Ok(new { status = "healthy" });
}
```

### Dependency injection and startup

Implement `ISinchFunctionInit` to register services, configure middleware, or map extra routes. The runtime discovers it automatically — there is no `Program.cs`.

```csharp
public class FunctionInit : ISinchFunctionInit
{
    public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
    {
        services.AddScoped<ICustomerService, CustomerService>();
        services.AddHttpClient<IMyApiClient, MyApiClient>();
    }

    public void ConfigureApp(SinchWebApplication app)
    {
        app.LandingPageEnabled = true;
        app.MapGet("/custom", () => Results.Ok(new { status = "ok" }));
    }
}
```

Your middleware runs after the platform middleware, so billing and logging cannot be bypassed.

## Node extension points

### `setup()` — startup hook and WebSockets

Node functions can export a `setup()` function that runs once before the server accepts requests. Use it for initialization and to register WebSocket endpoints (for `connectStream` in SVAML).

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

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

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

`SinchRuntime` is intentionally narrow — you cannot reach the Express app or add middleware. That is on purpose: the runtime guarantees platform middleware (billing, logging, validation) runs regardless of what your function does.

## Multi-file Node projects

`function.ts` is the entry point, and all endpoints must be exported from it. Other files in the project root are internal modules you import from.

```
function.ts          ← entry point, all exports live here
harness.ts           ← shared business logic
voice.ts             ← voice handler implementations
api.ts               ← REST endpoint implementations
assets/              ← private files (context.assets())
public/              ← static files (served at /)
```

Relative imports use `.js` extensions because the shared tsconfig uses NodeNext:

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

Keep all source files at the project root — the runtime expects them flat.

## When something goes wrong

On startup, both runtimes log the set of detected handlers:

```
Detected functions:
  POST /webhook/conversation  → conversationWebhook
  GET  /api/health            → health
  POST /ice                   → ice (voice)
```

If a request hits a path with no matching export, the error response lists the handlers you do have. That is almost always the first thing to check when a callback is not firing — make sure Sinch's callback URL matches a handler name, and make sure it is actually exported.

## Related

- [Voice callbacks](/docs/functions/functions/concepts/voice-callbacks) — ICE/ACE/PIE/DICE lifecycle and payloads
- [SVAML](/docs/functions/functions/concepts/svaml) — what voice handlers return
- [Context object](/docs/functions/functions/concepts/context-object) — the `context` argument every handler receives
- [Node.js runtime](/docs/functions/functions/runtimes/nodejs) / [C# runtime](/docs/functions/functions/runtimes/csharp) — per-runtime reference