How I secured my MVP Summit spot using automation

Following receiving my MVP Award last month, I really wanted to go to the Microsoft MVP Summit in Redmond next year . However, the number of seats for In-Person attendance is limited, and you never actually know when registration opens – until it does.

I decided this was a great opportunity for me to test the Azure OpenAI services. The idea was to get the HTML body of the website which announces registration and make GPT parse the text and respond whether registration was open or not.

Azure Functions was a perfect fit for this. A Flex Consumption app that runs every 5 minutes with a timer trigger is one of the easiest things in the world to setup, and it has a very low cost.

Finally, I needed some way to get notified when registration opens. I considered using Azure Communication Services to send me a text message or a call, but it was too much of a hassle for this small project, as you need to register a phone number with a provider and pay for that number.
Instead, I remembered that Home Assistant offers webhooks for automations, and since I have a Home Assistant Server at home and the Home Assistant companion app on my phone, that was a perfect match for this use case.

The Azure Function

The function runs on a timer every 5 minutes and performs the following steps:

  • Download HTML from the summit page
  • Run a fast keyword check (just because you can)
  • If inconclusive, call Azure OpenAI to classify the body content as OPEN or CLOSED
  • If OPEN, trigger a Home Assistant webhook which drives a mobile notification

Prerequisites

  • Azure OpenAI resource + deployed model (I used gpt-5-mini)
  • Function App (or local Function Core Tools) targeting .NET 8 isolated worker
  • Configured environment variables: AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY, AZURE_OPENAI_DEPLOYMENT
  • Home Assistant instance with an automation webhook URL and public access (I use Nabu Casa)
  • You must add the Azure.AI.OpenAI NuGet package to your project

Heuristic check

The heuristic avoids an AI call when obvious registration phrases exist. These were just a guess from my side and you could put in anything in here.

private bool HeuristicCheck(string html)
{
    var lowered = html.ToLowerInvariant();
    var indicators = new[]
    {
        "register now",
        "registration open",
        "register today",
        "sign up"
    };
    return indicators.Any(p => lowered.Contains(p));
}

Azure OpenAI classification

First, setup an AzureOpenAIClient. I did it in Program.cs:

builder.Services.AddSingleton(sp => new AzureOpenAIClient(
    new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")),
    new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY"))));

<body> section is sent, which reduces token usage:

private async Task<bool> DetermineRegistrationOpenAsync(string html, CancellationToken ct)
{
    if (HeuristicCheck(html)) return true;
    if (MissingOpenAiConfig()) return false;

    var body = ExtractBodyContent(html) ?? html;
    var chatClient = azureOpenAIClient.GetChatClient(_openAiDeployment);

    var messages = new List<ChatMessage>
    {
        new SystemChatMessage("You classify event registration state based on HTML body content. Reply only with OPEN or CLOSED."),
        new UserChatMessage(body)
    };

    var response = await chatClient.CompleteChatAsync(messages, ct);
    var answer = response.Value.Content.First().Text.Trim().ToUpperInvariant();
    return answer == "OPEN";
}

Webhook notification

When registration is detected as open, a JSON POST to the Home Assistant webhook triggers a mobile push notification:

private async Task TriggerWebhookAsync(CancellationToken ct)
{
    var payload = new { message = "Microsoft Summit registration appears OPEN.", url = SummitUri.ToString(), detectedAtUtc = DateTime.UtcNow };
    using var response = await _httpClient.PostAsJsonAsync(WebhookUri, payload, ct);
    if (!response.IsSuccessStatusCode)
        _logger.LogWarning("Webhook call failed: {Status}", response.StatusCode);
}

Configuration (local.settings.json)

{
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AZURE_OPENAI_ENDPOINT": "https://<your-openai-resource>.openai.azure.com/",
    "AZURE_OPENAI_KEY": "<key>",
    "AZURE_OPENAI_DEPLOYMENT": "gpt-5-mini"
  }
}

The Home Assistant automation

Create a new automation using the Webhook trigger. Here is my source:

alias: Summit registration
description: ""
triggers:
  - trigger: webhook
    allowed_methods:
      - POST
      - PUT
    local_only: false
    webhook_id: summit-registration-OMITTED
conditions: []
actions:
  - device_id: OMITTED
    domain: mobile_app
    type: notify
    title: Summit registration open!
    message: Registration is open
    data:
      actions:
        - action: URI
          title: Open Url
          uri: https://summit.microsoft.com/en-us/
mode: single

Security notes

  • Webhook URL should be treated as a secret.

Final notes

  • Most of my code was written with GitHub copilot in Agent mode with GPT-5
  • My costs to run this for a few days, including the Azure AI Foundry containing the gpt-5-mini model, added up to ~0.1 USD per day. And since it was announced one week ahead that registration would open some time this week, it added up to less than $1 to have this running for me.

Tips for moving Azure Functions from Premium plans to Flex Consumption

Azure Flex Consumption Functions went GA in November 2024. We have been using them extensively since they came out in public preview in May 2024, and we are very happy about this addition to the Azure Function SKU options.

When considering moving your Azure Functions to Flex Consumption, some limitations must be considered. These are described in the Flex Consumption hosting documentation.
However, in some cases, the limitations can be mitigated. Below, I will describe how to mitigate some of the limitations with very few lines of code.

Load certificates for HttpClients

In Flex Consumption, it is currently not supported to load certificates from the managed certificate store using the app setting linked to an Azure Key Vault certificate.

However, you can still access Key Vault certificates from your Functions in Flex Consumption if you need to.

A very common use case is to load the certificates from a key vault to use them with client certificate authentication in an HttpClient. To solve this use case in a Flex Consumption app, you can refer to the certificate in from a key vault using the secret URL of the certificate. If you refer to the secret URL of a key vault certificate, you get the certificate as a Base64-encoded string, which you can decode and add as a client certificate when you configure your client.
In your app settings, e.g. in Bicep, refer to the secret like this:

Settings__Certificate: '@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mycertificate)'

Notice the /secrets/ path in the URL and not /certificates/. Even though this is stored as a certificate in the vault, you can access it through the secrets API to retrieve it as a Base64 encoded string.

Then, using the Base64 encoded string in an app setting, you configure the HttpClient as follows in your Program.cs:

using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
	.ConfigureFunctionsWebApplication()
	.ConfigureServices(s =>
	{
		s.AddOptionsSection<MySettings>("Settings");
		var settings = s.GetOptions<MySettings>();

		s.AddHttpClient<MyService>()
			.ConfigurePrimaryHttpMessageHandler(() =>
			{
				var handler = new HttpClientHandler { UseCookies = true, ClientCertificateOptions = ClientCertificateOption.Manual };
				var certificate = new X509Certificate2(Convert.FromBase64String(settings.Certificate));
				handler.ClientCertificates.Add(certificate);
				return handler;
			});
	})
	.Build();

await host.RunAsync();

Cipher suites

On Flex Consumption, I have found that the set of default TLS Cipher Suites is more restrictive than what is configured per default in Consumption and Premium plans.
If you see errors from the runtime like Interop+Crypto+OpenSslCryptographicException and The SSL connection could not be established, see inner exception. Authentication failed, see inner exception. SSL Handshake failed with OpenSSL error - SSL_ERROR_SSL. error:0A000410:SSL routines::sslv3 alert handshake failure when calling and external API’s that do not support TLS1.3, I suggest you try the following:

Use ssllabs.com to get the list of supported cipher suites by the external endpoint: https://www.ssllabs.com/ssltest/

WARNING: The code below will modify, and, most likely, weaken, the security of your outbound HTTP calls. Only do this if you have to, and have no control over cipher suites of the remote API.

Configure the TLS Protocol and Cipher Suites on the HttpClient handler using the SocketsHttpHandler as shown below:

using System.Net.Security;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
  .ConfigureFunctionsWebApplication()
  .ConfigureServices(s =>
  {
    s.AddHttpClient("tls1.1").ConfigurePrimaryHttpMessageHandler(() =>
    new SocketsHttpHandler()
    {
      SslOptions = new SslClientAuthenticationOptions
      {
        EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13,
        CipherSuitesPolicy = new CipherSuitesPolicy(
        new[]
        {
          // Default
          TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
          TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
          TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
          TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,

          // Additional suites (replace with your own)
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
          TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
          TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
          TlsCipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
          TlsCipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
          TlsCipherSuite.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
          TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
          TlsCipherSuite.TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA,
          TlsCipherSuite.TLS_AES_128_GCM_SHA256,
        }),
      },

    });
  })
  .Build();

await host.RunAsync();

Accessing network-restricted Key Vaults

A current limitation of Flex Consumption at the time of writing (April 2025) is that you are unable to access secrets in a network-restricted Azure Key Vault through by using the SecretUri reference in the app settings of the Function App as follows: Microsoft.KeyVault(@SecretUri=https://myvault.vault.azure.net/secrets/mysecret)

Instead, you can use the Key Vault SDK to retrieve the secrets from code with the following code in your Program.cs:

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        var keyVaultUri = "https://myvault.vault.azure.net/";
        services.AddSingleton(new SecretClient(new System.Uri(keyVaultUri), new DefaultAzureCredential()));
    })
    .Build();

await host.RunAsync();

Now, you can inject the SecretClient into your services where needed.

I expect this to be a temporary limitation, but now you have a much better alternative than removing the network restrictions from the Key Vault.