You'll end up with: a GET /status endpoint that your function exposes, returning JSON. Same pattern works for any REST-style route you want to add.
Sinch Functions are mostly about reacting to voice and messaging events, but the underlying runtime is a regular HTTP server. You can expose any number of custom endpoints alongside your voice or conversation handlers — for health checks, admin APIs, external webhooks (Stripe, Shopify, ElevenLabs), or whatever else you need.
Every named export on your default object becomes an HTTP endpoint. The last segment of the request path is matched to an export name:
| Request path | Export called |
|---|---|
GET /status | status |
GET /api/health | health |
POST /api/v2/users | users |
ice, ace, pie, dice, notify are always voice callbacks regardless of path. Everything else is a custom HTTP endpoint. See handlers for the full mapping rules.
Extend SinchController (for custom endpoints) or add regular MVC controller actions alongside your voice controller. ASP.NET routing works the normal way.
// function.ts
import type { VoiceFunction, FunctionContext, FunctionRequest } from '@sinch/functions-runtime';
import { IceSvamlBuilder } from '@sinch/functions-runtime';
export default {
// Voice callback — POST /ice
async ice(context, data) {
return new IceSvamlBuilder().say('Welcome!').hangup().build();
},
// Custom endpoint — GET /status
async status(context: FunctionContext, request: FunctionRequest) {
return {
statusCode: 200,
body: {
status: 'healthy',
functionName: context.config.functionName,
environment: context.config.environment,
uptime: process.uptime(),
},
};
},
// Custom endpoint — POST /inbound
// Note: do NOT use `notify` as an export name — it's reserved for voice callbacks.
async inbound(context: FunctionContext, request: FunctionRequest) {
const body = request.body as { message?: string };
if (!body.message) {
return { statusCode: 400, body: { error: 'Missing message' } };
}
context.log?.info('Notification received', { message: body.message });
return { statusCode: 200, body: { received: true } };
},
} satisfies VoiceFunction;Custom endpoints return a plain { statusCode, body, headers? } object.
// StatusController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SinchFunctions.Utils;
[Route("api")]
public class StatusController : SinchController
{
public StatusController(FunctionContext context, IConfiguration config, ILogger<StatusController> logger)
: base(context, config, logger) { }
[HttpGet("status")]
public IActionResult GetStatus() =>
Ok(new
{
status = "healthy",
environment = Configuration["ASPNETCORE_ENVIRONMENT"],
});
[HttpPost("notify")]
public async Task<IActionResult> Notify([FromBody] NotifyRequest req)
{
if (string.IsNullOrWhiteSpace(req.Message))
return BadRequest("Missing message");
Logger.LogInformation("Notification received: {Message}", req.Message);
await Context.Cache.Set($"notify:{Guid.NewGuid()}", req, 3600);
return Ok(new { received = true });
}
}
public record NotifyRequest(string Message);The controller lives alongside your voice controller — you do not need to pick one or the other.
Custom endpoints get the same context / Context as voice callbacks, so you have cache, storage, database, logger, and pre-wired SDK clients available. Example — rate-limiting a webhook by IP using the cache:
async inbound(context, request) {
const ip = request.headers?.['x-forwarded-for'] ?? 'unknown';
const key = `ratelimit:${ip}`;
const count = (await context.cache.get<number>(key)) ?? 0;
if (count >= 10) return { statusCode: 429, body: { error: 'rate limited' } };
await context.cache.set(key, count + 1, 60);
// ... handle the notification
return { statusCode: 200, body: { received: true } };
}Custom endpoints are public by default — anyone on the internet can hit them. For anything sensitive, add authentication:
- Basic Auth. The runtime supports a declarative
authexport in Node. See protect your function. - Bearer token. Read the token from
request.headers?.authorizationin Node, or use[Authorize]in C# with your own auth handler. Never accept tokens as query parameters — query strings end up in access logs. - Webhook signature. For external webhooks (Stripe, Shopify, etc.), verify the signature on the body before processing.
- Private deploy.
sinch functions deploy --privatemakes the whole function reachable only from within Sinch services.
Start the dev server and hit the endpoint with curl:
sinch functions dev
# In another terminal
curl http://localhost:3000/status
curl -X POST http://localhost:3000/inbound \
-H "Content-Type: application/json" \
-d '{"message":"hello"}'Custom endpoints are tunneled the same way voice callbacks are — if the CLI opens a public tunnel, the endpoint is reachable from the internet at the tunnel URL during sinch functions dev.
- Handlers concept — URL-to-handler mapping rules
- Context object — the
contextAPIs you can use in custom endpoints - Protect your function — authentication for custom endpoints
- Node.js runtime / C# runtime — runtime reference