Skip to main content
  1. Writing/

How I Automated My Vaccine Appointment Search

·1222 words

Recently, younger residents of British Columbia (BC) became eligible to get vaccinated against COVID-19, which is awesome news! If you live in BC, I encourage you to register to get vaccinated as soon as possible.

As I was getting ready to register myself, I quickly learned that all of the appointments that were available near me were all the way in June, which is still some time out. Not terribly bad, but I was wondering if I can get my first shot sooner. I’ve heard about folks who were cancelling their existing appointments and therefore making their previous time slots available, but I had no visibility into the process on the vaccination registration site. Another tricky part with the local vaccination appointment website was that once I registered, I could not easily switch appointments without cancelling the existing one that I had, which meant that I could potentially delay my vaccination even further.

For me to find the right time slot using the site, I needed to constantly re-type nearby city names, select one of the available facilities, then select the day, and finally - see if there are any times that fit my schedule.

Example of searching for a vaccine appointment

As you can see - not the most efficient workflow for finding up-to-date appointments that are in the near future quickly. As any engineering would do, I decided that I was too lazy to spend time mindlessly clicking around, so instead I thought that maybe I can use my programming chops to automate this process.

What I wrote does not do anything with registration - it merely finds available appointments for a location using the same Application Programming Interface (API) that the website is using. To register for a vaccine, you will need a valid registration key and your Personal Health Number (PHN), both managed by the Government of BC.

My first step was to look at how the registration site works. Using the network inspector in my web browser, I realized that there are three things happening when entering a city in the search box:

  1. The service gets a list of facilities for the location if the city/town is valid.
  2. For a selected facility, the service then gets a list of available days.
  3. Lastly, for a selected day, the service gets a list of appointment time blocks.

All of these steps were done through their own, separate HTTP API calls. Through the network inspector in the web browser, I saw that the requests were all going to the same URL:

https://www.getvaccinated.gov.bc.ca/s/sfsites/aura?r=11&aura.ApexAction.execute=1

Looked fairly simple - POST requests that sent application/x-www-form-urlencoded content, that in turn contained three key components:

  1. A JSON message, that tells the service what information it needs to get.
  2. An aura.context JSON blob, that contains application information.
  3. An aura.token value, that is undefined.

None of the requests required any authentication or credentials. What that meant is that I can wrap them in some C# code, and issue the calls in bulk instead of clicking around on every single facility on the web page. I could also substitute city values and search for another location near me to get the latest vaccine appointment.

My entire console application ended up looking like this:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("BC Vaccine Finder");
        MainAsync().Wait();
    }

    static async Task MainAsync()
    {
        var region = "Vancouver";
        BCVaccinationAPIHelper helper = new BCVaccinationAPIHelper();
        Console.WriteLine($"Looking for facilities in the specified region: {region}");

        var facilities = await helper.GetFacilities(region);
        foreach (var facility in facilities.actions.First().Facilities.FacilityCollection)
        {
            Console.WriteLine($"[{facility.Name}] ({facility.DDH__HC_Primary_Address_1__c})");

            var facilityAppointments = await helper.GetFacilityDays(facility.Id);
            if (facilityAppointments.actions.First().Appointments.VaccinationFacilityAppointments != null &&
                facilityAppointments.actions.First().Appointments.VaccinationFacilityAppointments.Length > 0)
            {
                Console.WriteLine("For this facility, the following days are available this month:");
                foreach (var day in facilityAppointments.actions.First().Appointments.VaccinationFacilityAppointments)
                {
                    Console.WriteLine(day.DDH__HC_Appointments_Date__c);

                    var timeBlocks = await helper.GetTimeBlocks(day.Id, facility.Id);
                    foreach(var timeBlock in timeBlocks.actions.First().AppointmentBlockCollection.AppointmentBlocks)
                    {
                        var timeSpan = TimeSpan.FromMilliseconds(timeBlock.DDH__HC_Start_Time__c);
                        Console.Write($"{timeSpan.Hours}:{timeSpan.Minutes} ");
                    }
                    Console.WriteLine();
                }
            }
            else
            {
                Console.WriteLine("[!] No appointments available at this facility for current month.");
            }
        }
        Console.ReadKey();
    }
}

Keep in mind that because the program is so simple and I had very little time to put this all together, there is no proper exception handling or any kind of performance optimizations. I just wanted to get a working prototype going.

You might’ve noticed that the heavy lifting here is done by the BCVaccinationAPIHelper class, that wraps the messages that need to be sent to the appointment service. It ended up being much less complex than I expected:

public class BCVaccinationAPIHelper
{
    private string? VaccinationUrl = "https://www.getvaccinated.gov.bc.ca/s/sfsites/aura?r=11&aura.ApexAction.execute=1";
    private string FacilityMessage = "{{\"actions\":[{{\"id\":\"147;a\",\"descriptor\":\"aura://ApexActionController/ACTION$execute\",\"callingDescriptor\":\"UNKNOWN\",\"params\":{{\"namespace\":\"\",\"classname\":\"BCH_SchedulerController\",\"method\":\"getFacilities\",\"params\":{{\"territory\":\"{0}\",\"priorityCode\":\"\"}},\"cacheable\":true,\"isContinuation\":false}}}}]}}";
    private string SpecificFacilityMessage = "{{\"actions\":[{{\"id\":\"149;a\",\"descriptor\":\"aura://ApexActionController/ACTION$execute\",\"callingDescriptor\":\"UNKNOWN\",\"params\":{{\"namespace\":\"\",\"classname\":\"BCH_SchedulerController\",\"method\":\"getAppointmentDays\",\"params\":{{\"facility\":\"{0}\",\"appointmentType\":\"COVID-19 Vaccination\"}},\"cacheable\":true,\"isContinuation\":false}}}}]}}";
    private string AppointmentBlockMessage = "{{\"actions\":[{{\"id\":\"156;a\",\"descriptor\":\"aura://ApexActionController/ACTION$execute\",\"callingDescriptor\":\"UNKNOWN\",\"params\":{{\"namespace\":\"\",\"classname\":\"BCH_SchedulerController\",\"method\":\"getAppointmentBlocks\",\"params\":{{\"appointmentDay\":\"{0}\", \"facility\":\"{1}\",\"appointmentType\":\"COVID-19 Vaccination\"}},\"cacheable\":true,\"isContinuation\":false}}}}]}}";
    private string AuraContext = "{\"mode\":\"PROD\",\"fwuid\":\"SOME_FWUID\",\"app\":\"siteforce:communityApp\",\"loaded\":{\"APPLICATION@markup://siteforce:communityApp\":\"APP_ID\"},\"dn\":[],\"globals\":{},\"uad\":false}";
    private string AuraToken = "undefined";

    public async Task<AuthoritativeFacilityList> GetFacilities(string region)
    {
        List<KeyValuePair<string, string>> _requestValues = new List<KeyValuePair<string, string>>();
        _requestValues.Add(new KeyValuePair<string, string>("message", string.Format(FacilityMessage, region)));
        _requestValues.Add(new KeyValuePair<string, string>("aura.context", AuraContext));
        _requestValues.Add(new KeyValuePair<string, string>("aura.token", AuraToken));

        return await SendRequest<AuthoritativeFacilityList>(_requestValues);
    }

    public async Task<AuthoritativeVaccinationFacility> GetFacilityDays(string facilityId)
    {
        List<KeyValuePair<string, string>> _requestValues = new List<KeyValuePair<string, string>>();
        _requestValues.Add(new KeyValuePair<string, string>("message", string.Format(SpecificFacilityMessage, facilityId)));
        _requestValues.Add(new KeyValuePair<string, string>("aura.context", AuraContext));
        _requestValues.Add(new KeyValuePair<string, string>("aura.token", AuraToken));

        return await SendRequest<AuthoritativeVaccinationFacility>(_requestValues);
    }

    public async Task<VaccinationBlock> GetTimeBlocks(string dayId, string facility)
    {
        List<KeyValuePair<string, string>> _requestValues = new List<KeyValuePair<string, string>>();
        _requestValues.Add(new KeyValuePair<string, string>("message", string.Format(AppointmentBlockMessage, dayId, facility)));
        _requestValues.Add(new KeyValuePair<string, string>("aura.context", AuraContext));
        _requestValues.Add(new KeyValuePair<string, string>("aura.token", AuraToken));

        return await SendRequest<VaccinationBlock>(_requestValues);
    }

    private async Task<T> SendRequest<T>(List<KeyValuePair<string, string>> messages)
    {
        var client = new HttpClient();
        var request = new HttpRequestMessage(HttpMethod.Post, VaccinationUrl)
        {
            Content = new FormUrlEncodedContent(messages)
        };

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            var result = response.Content.ReadAsStringAsync().Result;
            return JsonSerializer.Deserialize<T>(result);
        }
        else
        {
            return default(T);
        }
    }
}

Remember the three calls that I outlined above? This class implements all of them. For each method, a special string is used, that is formatted on the fly with the information from the program snippet I outlined above, such as the facility ID or the day ID. Classes such as AuthoritativeFacilityList and AuthoritativeVaccinationFacility are nothing more than a nicely wrapped object for the JSON that is being returned by the vaccination appointment service. And those can be generated automatically with Visual Studio if you copy and paste the JSON content you see flowing from your browser to the service through the Paste Special functionality.

I intentionally did not include application context in the request above, since I am not certain whether it includes my own information, but it can be read if you look in your web browser network inspector, under the aura.context field being passed to the service.

Running the simple console application, the calls to the service were sequentially executed, and I got a full list of available appointments at the time when the application ran.

Running console application that searches for vaccine appointments

Now this is time-efficient, and really making my skills useful for something - finding when someone cancelled an appointment, which allowed me to cancel my existing appointment and book one closer time-wise. You can check out the project on GitHub.

The big question - did it work? Indeed - I managed to find an earlier appointment that was made available by someone cancelling, and I got my first shot just the other day.

Den standing by the water in Vancouver, with a mask on and a sticker that says &ldquo;I&rsquo;m COVID-19 vaccinated&rdquo;

Finally feeling hopeful about where we’re going with the pandemic.

Instead of some conclusion, I just want to say thank you to all the healthcare and essential workers, the scientists, and the many people that did and continue to do their part in helping us get through this, hardly easy, time. A special shout-out to my wife Tiffany, who helped me properly inspect the data.

Get vaccinated (Canada/United States) 💉.