{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-docs/functions/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["partial"]},"type":"markdown"},"seo":{"title":"Developer Documentation","siteUrl":"https://developers.sinch.com","llmstxt":{"title":"Sinch Developer Documentation","description":"LLMs.txt containing a map of all the documentation files for Sinch.","sections":[{"title":"SMS API","description":"The SMS API allows you to send and receive SMS messages with a few easy steps. You can also send bulk SMS messages to multiple customers using the Sinch SMS service.","includeFiles":["docs/sms/**/*.md","docs/sms/**/*.yaml"],"excludeFiles":["docs/sms/index.md"]},{"title":"Numbers API","description":"The Numbers API enables you to search for, view, and activate numbers. It's considered a precursor to other APIs in the Sinch product family. The numbers API can be used in tandem with any of our APIs that perform messaging or calling.","includeFiles":["docs/numbers/**/*.md","docs/numbers/**/*.yaml"],"excludeFiles":["docs/numbers/index.md"]},{"title":"Conversation API","description":"Send and receive messages globally on many popular channels with ease and confidence when using Sinch's Conversation API. Conversation API is the preferred API for sending mobile messages on SMS and other social channels with Sinch. It is a simple API with unified error messages, consistent request payloads, and common webhook payloads that are channel-agnostic.","includeFiles":["docs/conversation/**/*.md","docs/conversation/**/*.yaml"],"excludeFiles":["docs/conversation/index.md"]},{"title":"Voice API","description":"The Voice API works as a big telephony switch. The Voice API handles incoming phone calls (also known as incoming call “legs”), sets up outgoing phone calls (or outgoing call “legs”), and bridges the two. The incoming call leg may come in over a data connection (from a smartphone or web application using the Sinch SDKs) or through a local phone number (from the PSTN network). Similarly, the outgoing call leg can be over data (to another smartphone or web application using the Sinch SDKs) or the PSTN network.","includeFiles":["docs/voice/**/*.md","docs/voice/**/*.yaml"],"excludeFiles":["docs/voice/index.md"]},{"title":"Verification API","description":"The Verification API is a platform for phone number verification. It consists of the API and different software development kits (the Sinch SDKs) that you integrate with your smartphone or web application and cloud based back-end services. Together they enable SMS, Flashcall, Phone Call and Data verification in your application.","includeFiles":["docs/verification/**/*.md","docs/verification/**/*.yaml"],"excludeFiles":["docs/verification/index.md"]},{"title":"Provisioning API","description":"Provisioning API allows you to programmatically set up your senders, accounts and templates on your favorite messaging platforms on the Conversation API. For now, you can create your first WhatsApp channel through Meta's Embedded sign up, you can configure your first SMS App and configure your webhooks. As development continues, we will be adding the most commonly used channels.","includeFiles":["docs/provisioning-api/**/*.md","docs/provisioning-api/**/*.json"],"excludeFiles":["docs/provisioning-api/index.md"]},{"title":"Elastic SIP Trunking API","description":"With Elastic SIP Trunking you can create and manage your SIP trunks and phone numbers programmatically.","includeFiles":["docs/est/**/*.md","docs/est/**/*.yaml"],"excludeFiles":["docs/est/index.md"]},{"title":"Fax API","description":"Send and receive HIPAA compliant faxes on our modern fax platform using our developer-friendly API.","includeFiles":["docs/fax/**/*.md","docs/fax/**/*.yaml"],"excludeFiles":["docs/fax/index.md"]},{"title":"In-app Voice and Video SDK","description":"The In-app Voice and Video SDK enables you to add voice and video calling capabilities directly into your mobile or web application using the Sinch SDKs.","includeFiles":["docs/in-app-calling/**/*.md"],"excludeFiles":["docs/in-app-calling/index.md"]}],"hide":false,"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"add-a-custom-http-endpoint","__idx":0},"children":["Add a custom HTTP endpoint"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You'll end up with: a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /status"]}," endpoint that your function exposes, returning JSON. Same pattern works for any REST-style route you want to add."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"why","__idx":1},"children":["Why"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-the-runtime-routes-requests","__idx":2},"children":["How the runtime routes requests"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"nodejs","__idx":3},"children":["Node.js"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every named export on your default object becomes an HTTP endpoint. The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["last segment of the request path"]}," is matched to an export name:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Request path"},"children":["Request path"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Export called"},"children":["Export called"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /status"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["status"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /api/health"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["health"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /api/v2/users"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["users"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ice"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ace"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pie"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dice"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["notify"]}," are always voice callbacks regardless of path. Everything else is a custom HTTP endpoint. See ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["handlers"]}," for the full mapping rules."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"c","__idx":4},"children":["C#"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Extend ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SinchController"]}," (for custom endpoints) or add regular MVC controller actions alongside your voice controller. ASP.NET routing works the normal way."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"nodejs-example","__idx":5},"children":["Node.js example"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"// function.ts\nimport type { VoiceFunction, FunctionContext, FunctionRequest } from '@sinch/functions-runtime';\nimport { IceSvamlBuilder } from '@sinch/functions-runtime';\n\nexport default {\n  // Voice callback — POST /ice\n  async ice(context, data) {\n    return new IceSvamlBuilder().say('Welcome!').hangup().build();\n  },\n\n  // Custom endpoint — GET /status\n  async status(context: FunctionContext, request: FunctionRequest) {\n    return {\n      statusCode: 200,\n      body: {\n        status: 'healthy',\n        functionName: context.config.functionName,\n        environment: context.config.environment,\n        uptime: process.uptime(),\n      },\n    };\n  },\n\n  // Custom endpoint — POST /inbound\n  // Note: do NOT use `notify` as an export name — it's reserved for voice callbacks.\n  async inbound(context: FunctionContext, request: FunctionRequest) {\n    const body = request.body as { message?: string };\n    if (!body.message) {\n      return { statusCode: 400, body: { error: 'Missing message' } };\n    }\n    context.log?.info('Notification received', { message: body.message });\n    return { statusCode: 200, body: { received: true } };\n  },\n} satisfies VoiceFunction;\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Custom endpoints return a plain ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ statusCode, body, headers? }"]}," object."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"c-example","__idx":6},"children":["C# example"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"csharp","header":{"controls":{"copy":{}}},"source":"// StatusController.cs\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing SinchFunctions.Utils;\n\n[Route(\"api\")]\npublic class StatusController : SinchController\n{\n    public StatusController(FunctionContext context, IConfiguration config, ILogger<StatusController> logger)\n        : base(context, config, logger) { }\n\n    [HttpGet(\"status\")]\n    public IActionResult GetStatus() =>\n        Ok(new\n        {\n            status = \"healthy\",\n            environment = Configuration[\"ASPNETCORE_ENVIRONMENT\"],\n        });\n\n    [HttpPost(\"notify\")]\n    public async Task<IActionResult> Notify([FromBody] NotifyRequest req)\n    {\n        if (string.IsNullOrWhiteSpace(req.Message))\n            return BadRequest(\"Missing message\");\n\n        Logger.LogInformation(\"Notification received: {Message}\", req.Message);\n        await Context.Cache.Set($\"notify:{Guid.NewGuid()}\", req, 3600);\n        return Ok(new { received = true });\n    }\n}\n\npublic record NotifyRequest(string Message);\n","lang":"csharp"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The controller lives alongside your voice controller — you do not need to pick one or the other."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"accessing-the-functioncontext","__idx":7},"children":["Accessing the FunctionContext"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Custom endpoints get the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["context"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["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:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"async inbound(context, request) {\n  const ip = request.headers?.['x-forwarded-for'] ?? 'unknown';\n  const key = `ratelimit:${ip}`;\n  const count = (await context.cache.get<number>(key)) ?? 0;\n\n  if (count >= 10) return { statusCode: 429, body: { error: 'rate limited' } };\n\n  await context.cache.set(key, count + 1, 60);\n  // ... handle the notification\n  return { statusCode: 200, body: { received: true } };\n}\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"protecting-your-endpoints","__idx":8},"children":["Protecting your endpoints"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Custom endpoints are public by default — anyone on the internet can hit them. For anything sensitive, add authentication:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Basic Auth."]}," The runtime supports a declarative ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["auth"]}," export in Node. See ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["protect your function"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Bearer token."]}," Read the token from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["request.headers?.authorization"]}," in Node, or use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["[Authorize]"]}," in C# with your own auth handler. Never accept tokens as query parameters — query strings end up in access logs."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Webhook signature."]}," For external webhooks (Stripe, Shopify, etc.), verify the signature on the body before processing."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Private deploy."]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch functions deploy --private"]}," makes the whole function reachable only from within Sinch services."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"testing-locally","__idx":9},"children":["Testing locally"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Start the dev server and hit the endpoint with curl:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"sinch functions dev\n\n# In another terminal\ncurl http://localhost:3000/status\ncurl -X POST http://localhost:3000/inbound \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"message\":\"hello\"}'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch functions dev"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"related","__idx":10},"children":["Related"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["Handlers concept"]}," — URL-to-handler mapping rules"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["Context object"]}," — the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["context"]}," APIs you can use in custom endpoints"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["Protect your function"]}," — authentication for custom endpoints"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["Node.js runtime"]}," / ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"#"},"children":["C# runtime"]}," — runtime reference"]}]}]},"headings":[{"value":"Add a custom HTTP endpoint","id":"add-a-custom-http-endpoint","depth":1},{"value":"Why","id":"why","depth":2},{"value":"How the runtime routes requests","id":"how-the-runtime-routes-requests","depth":2},{"value":"Node.js","id":"nodejs","depth":3},{"value":"C#","id":"c","depth":3},{"value":"Node.js example","id":"nodejs-example","depth":2},{"value":"C# example","id":"c-example","depth":2},{"value":"Accessing the FunctionContext","id":"accessing-the-functioncontext","depth":2},{"value":"Protecting your endpoints","id":"protecting-your-endpoints","depth":2},{"value":"Testing locally","id":"testing-locally","depth":2},{"value":"Related","id":"related","depth":2}],"frontmatter":{"seo":{"title":""}},"lastModified":"2026-04-15T14:23:23.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/functions/guides/add-a-custom-endpoint","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}