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 IsaSdkdotnet add package Microsoft.Azure.Functions.Workerdotnet add package Microsoft.Azure.Functions.Worker.Extensions.HttpISA_TOKENinlocal.settings.json→Valuesfor 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
Updated about 9 hours ago