Azure Functions (C#)

Run the ISA SDK in an Azure Functions app written in C#: DI singleton, isolated worker, and webhook trigger.

Azure Functions (C#)

This guide shows how to run the ISA SDK inside an Azure Functions app using the isolated worker model (the recommended .NET 8 model). The client is registered as a singleton in the DI container so it survives across warm invocations.

Prerequisites

  • .NET 8, Azure Functions Core Tools v4
  • dotnet add package IsaSdk
  • dotnet add package Microsoft.Azure.Functions.Worker
  • dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http
  • ISA_TOKEN in local.settings.jsonValues for local dev; Key Vault references for production

DI registration

// Program.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Isa.Sdk;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddSingleton(_ => IsaClient.WithBearer());
    })
    .Build();

host.Run();

IsaClient.WithBearer() activates the license on first use (one network round-trip to exchange for a session secret) and reuses the same instance for every invocation thereafter. Safe to call at startup.

Prequalify function

// Functions/PrequalifyFunction.cs
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Isa.Sdk;
using Isa.Sdk.Zyins;
using Catalog = Isa.Sdk.Catalog;

namespace MyFunctions;

public class PrequalifyFunction(Isa isa, ILogger<PrequalifyFunction> logger)
{
    [Function("Prequalify")]
    public async Task<HttpResponseData> RunAsync(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "prequalify")] HttpRequestData req,
        CancellationToken ct)
    {
        var input = new PrequalifyInput
        {
            Applicant = new Applicant
            {
                Dob          = "1962-04-18",
                Sex          = Sex.Male,
                HeightInches = 70,
                WeightPounds = 195,
                State        = Catalog.States.WireValue(Catalog.State.NorthCarolina),
                NicotineUse  = NicotineUsage.None,
            },
            Coverage = Coverage.ByFaceValue(25_000),
            Products = new[] { new Product("aetna", Catalog.Products.WireValue(Catalog.Product.FexAetnaAccendo)) },
        };

        try
        {
            var result = await isa.Zyins.Prequalify.RunAsync(input, ct);
            var ok = req.CreateResponse(HttpStatusCode.OK);
            await ok.WriteAsJsonAsync(new
            {
                plans      = result.Data.Plans.Select(p => new {
                    carrier         = p.Carrier.Name,
                    product         = p.Product.DisplayName,
                    category        = p.Eligibility.Category,
                    premium_display = p.Premium.Amount.Display,
                    premium_cents   = p.Premium.Amount.Cents,
                }),
                request_id = result.RequestId,
            }, ct);
            return ok;
        }
        catch (IsaValidationException ex)
        {
            logger.LogWarning("validation error: param={Param} message={Message}", ex.Param, ex.Message);
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteAsJsonAsync(new { error = ex.Message, param = ex.Param }, ct);
            return bad;
        }
        catch (IsaException ex)
        {
            logger.LogError("isa error: code={Code} requestId={RequestId}", ex.CodeEnum, ex.RequestId);
            var err = req.CreateResponse(HttpStatusCode.BadGateway);
            await err.WriteAsJsonAsync(new { code = ex.CodeEnum.ToString(), request_id = ex.RequestId }, ct);
            return err;
        }
    }
}

Webhook function

// Functions/WebhookFunction.cs
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

namespace MyFunctions;

public class WebhookFunction(ILogger<WebhookFunction> logger)
{
    private const int ToleranceSeconds = 300;

    [Function("IsaWebhook")]
    public async Task<HttpResponseData> RunAsync(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "webhooks/isa")] HttpRequestData req,
        CancellationToken ct)
    {
        var rawBody  = await new StreamReader(req.Body).ReadToEndAsync(ct);
        var sigHeader = req.Headers.TryGetValues("X-ZyINS-Signature", out var vals)
            ? vals.FirstOrDefault() ?? string.Empty
            : string.Empty;

        if (!VerifySignature(rawBody, sigHeader, Environment.GetEnvironmentVariable("ISA_WEBHOOK_SECRET") ?? string.Empty))
        {
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteStringAsync("invalid signature", ct);
            return bad;
        }

        // Deserialize and dispatch on event["type"]
        logger.LogInformation("received isa webhook: {Body}", rawBody);

        return req.CreateResponse(HttpStatusCode.OK);
    }

    // VerifySignature parses the "t=<unix>,v1=<hex>" X-ZyINS-Signature header,
    // recomputes HMAC-SHA256 over "<t>.<body>", and compares in constant time.
    private static bool VerifySignature(string rawBody, string sigHeader, string secret)
    {
        var fields = sigHeader.Split(',')
            .Select(part => part.Split('=', 2))
            .Where(kv => kv.Length == 2)
            .ToDictionary(kv => kv[0], kv => kv[1]);

        if (!fields.TryGetValue("t", out var tsField)
            || !fields.TryGetValue("v1", out var receivedHex)
            || !long.TryParse(tsField, out var tsLong))
            return false;
        if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - tsLong) > ToleranceSeconds)
            return false;

        var payload  = $"{tsField}.{rawBody}";
        var keyBytes = Encoding.UTF8.GetBytes(secret);
        var msgBytes = Encoding.UTF8.GetBytes(payload);
        var expected = HMACSHA256.HashData(keyBytes, msgBytes);
        var received = Convert.FromHexString(receivedHex);

        return CryptographicOperations.FixedTimeEquals(expected, received);
    }
}

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ISA_TOKEN": "<paste your isa_test_… key here>",
        "ISA_WEBHOOK_SECRET": "<your-webhook-secret>"
  }
}

In production, use Key Vault references (@Microsoft.KeyVault(SecretUri=...)) rather than embedding secrets directly.

See also