# Add a custom HTTP endpoint 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. ## Why 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. ## How the runtime routes requests ### Node.js 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. ### C# Extend `SinchController` (for custom endpoints) or add regular MVC controller actions alongside your voice controller. ASP.NET routing works the normal way. ## Node.js example ```typescript // 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. ## C# example ```csharp // 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 logger) : base(context, config, logger) { } [HttpGet("status")] public IActionResult GetStatus() => Ok(new { status = "healthy", environment = Configuration["ASPNETCORE_ENVIRONMENT"], }); [HttpPost("notify")] public async Task 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. ## Accessing the FunctionContext 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: ```typescript async inbound(context, request) { const ip = request.headers?.['x-forwarded-for'] ?? 'unknown'; const key = `ratelimit:${ip}`; const count = (await context.cache.get(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 } }; } ``` ## Protecting your endpoints 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 `auth` export in Node. See [protect your function](#). - **Bearer token.** Read the token from `request.headers?.authorization` in 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 --private` makes the whole function reachable only from within Sinch services. ## Testing locally Start the dev server and hit the endpoint with curl: ```bash 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`. ## Related - [Handlers concept](#) — URL-to-handler mapping rules - [Context object](#) — the `context` APIs you can use in custom endpoints - [Protect your function](#) — authentication for custom endpoints - [Node.js runtime](#) / [C# runtime](#) — runtime reference