Build a COVID-19 Vaccine Appointment Notification System with Twilio Serverless Functions

Illustration by Jason Kim

This guide will walk you through setting up a Twilio Serverless Function to notify you when COVID-19 Vaccine Appointments open up in your area.

This project currently supports the following systems:
- California: MyTurn
- Massachusetts: MA COVID Vaccines
- New York (NYC Only): NYC Vaccine List
- USA Nationwide (CVS, Rite-Aid, Walgreens): GetMyVaccine

GitHub Source Code: our-turn

[Update March 25, 2021]: MyTurn’s API is constantly changing as requirements are updated so the source code and schema in this project may need to be adapted to align with the changes since this guide was written. Take note of the eligibility API call and the names and acceptable values of the fields — this is most likely what will change over time. This project has also been adapted to a more scalable and cheaper architecture with AWS Lambda. You can dig into details and source code here.

Our Problem

States don’t have reliable and centralized listings where you can find out where COVID-19 vaccines are being administered and whether or not they have available doses. Thanks to volunteer-led efforts, we have sites like TurboVax, NYC Vaccine List, MA COVID Vaccines, VaccinateCA, GetMyVaccine, and others who are trying to bridge this information gap. However, you still need to constantly monitor these sites for updates and can’t get notified as soon as appointments are posted.

Our Solution

With Twilio Functions, we can create a simple notification layer on top of these aggregation services to provide timely updates via e-mail or phone call when appointments become available. This can easily be adapted further to support alerts via SMS messages.

  • Twilio Functions for serverless functions
  • Twilio Voice for phone notifications
  • Twilio SendGrid for e-mail notifications
  • Airtable as a database
  • cronhooks.io as a cron job scheduler
Architecture Overview

Setup Your Accounts

Note: You will need to verify your email address and it will need to be on your own domain (not gmail) due to DMARC. I haven’t explored other alternatives for personal-use API-driven email messaging, but you could skip the email functionality and only implement phone support with Twilio Voice.

  1. Sign up for an account on SendGrid. Although SendGrid is now a Twilio product, you’ll need to create a separate account and use a separate API key just for SendGrid.

2. Once you’ve gone through the onboarding and have your account verified, create an API key for this project. Use a Restricted Access key to only enable Mail sending.

Restrict this API key’s access to just sending Mail

3. Once your API key is generated, copy that and store it in a safe place. You won’t be able to view it again after exiting the screen. You will need to add this key to the Environment Variables in your Twilio function.

  1. Sign up for an account on Airtable (Referral Link). We’ll use Airtable as a database solution to keep track of multiple recipients to notify and some user preferences (which location, which dose, etc.)

2. Create a new Base from scratch

3. Name your base and import the sample CSV schema in the relevant sub-project (schema is tied to the different state systems). Optionally, rename your table from Imported Table to something else (this will be part of your API endpoint URL)

4. Fix the field types as Airtable will try to guess the field types but it may not be accurate.

Ensure that (some fields may not be relevant to your state system):

  • Name, Email, Dose, Age Range, and Industry have the type Single Line Text
  • Locations has the type Long Text
  • Email has type Email
  • Phone number has type Phone Number and ensure your user phone number values are in the format: +14151234567
  • Last Call and Last Email have the type Date (include a time field, use 24 hour time format, and use GMT)
  • Min Call Threshold and Min Email Threshold have the type Number (Integer, do not allow negative numbers)

You should have something like the image below once you change the values to your contact information. If you only want to have e-mails and no phone calls, just leave the phone field empty. Last call and Last email will get auto-populated the first time a notification is sent.

Example User Record for NYC monitor

Minimum Call Threshold and Minimum Email Threshold
These are time values (in minutes) expressed as a Number. It represents the minimum amount of time that must pass in between successive notifications (phone calls or e-mail messages). Let’s say our serverless function runs every 5 minutes but there is
consistently an availability for the user. Without this, we would call the user every 5 minutes. If you only want to be called at most once a day, you could set this to 1440 (1440 minutes in a day).

5. Get your Airtable API key and Base Table API Endpoint

Go to your Airtable Account Settings and under the API section, you’ll find your Airtable API key.

Go to your Airtable API Settings and select the Base you just made. The AIRTABLE_API_ENDPOINT environment variable we need to populate in our Twilio function should be in the following format:

https://api.airtable.com/v0/YOUR_AIRTABLE_BASE_ID/YOUR_AIRTABLE_TABLE_NAME

You should see an example of this in the Authentication section of the API docs. If your Airtable Base ID were abcdef1234 and Table name were NYC, your endpoint would be https://api.airtable.com/v0/abcdef1234/NYC.

  1. Sign up for a trial account on Twilio. (Referral link for $10 credit)
Twilio Signup Page
Twilio Signup Page

2. Verify your e-mail and phone number with Twilio and follow through the onboarding flow. After the onboarding, you should see your main Twilio Console with a Trial balance.

4. You’ll need to upgrade your account and purchase a phone number to use. Refer to Twilio’s docs for how to setup your account with a phone number here.

You can likely remain on a free trial account and work within its limitations if you are only calling yourself. You’ll need to verify your own phone number to enable outbound calling to it and your programmed phone message will only play after you press a key.

5. Navigate to Twilio Functions to create your first Service.

Twilio Functions Dashboard
Create a Service, which will host your function. (A service can have multiple “serverless” functions)

6. Add a new Function and name your endpoint. This will be part of your webhook URL: https://covid-monitor-123.twil.io/your_endpoint_name

You can choose a long, random alphanumeric string if you want to make your endpoint obscure. Since we’re triggering our webhook outside of the Twilio ecosystem, this function will need to be “Public” meaning anyone who knows your URL could trigger the function. At the time of writing, Twilio Functions do not yet support accessing the request headers in the function execution context. In the future, you could validate a signature in an HTTP header to ensure cronhooks.io is calling and reject requests from unknown clients.

7. Add the function code!

For whichever system you are monitoring (CA, NY, etc.), copy the source code from the JS file in the functions directory of the relevant sub-project and replace all the boilerplate Hello World code on the screen.

8. Setup Dependencies

Add axios as a dependency, as shown in the screen. For NYC Vaccine List and GetMyVaccine, there is an additional dependency on cheerio . (I’m using axios @ 0.21.1 and cheerio @ 1.0.0-rc.5 ) Click Add and once you click Deploy All, all the other default dependencies will be loaded. You’ll only need to do this once at the very beginning during setup.

9. Setup Environment Variables

Add in the credentials for all the services we’re using. Refer to an example .env file in the source code for a list of all the values you’ll need to add.

For reference:

  • TWILIO_TWIML_BIN_ID is an alphanumeric string AbC123def456 (set up instructions below)
  • TWILIO_PHONE_NUMBER is your Twilio account’s phone number, not the recipient’s phone number (e.g. +14151234567)
  • SENDGRID_SENDER is your SendGrid account’s sender e-mail address from which you are e-mailing (e.g. test@example.com)
  • AIRTABLE_API_KEY is a standard Airtable API key
  • AIRTABLE_API_ENDPOINT is the URL for your Airtable base table. Refer to Airtable REST API docs. (e.g. https://api.airtable.com/v0/YOUR_AIRBASE_BASE_UNIQUE_ID/YOUR_AIRBASE_TABLE_NAME)
  • SENDGRID_API_KEY is a standard SendGrid API key

7. Click Deploy All to deploy your service and your function and then copy the URL. Ensure the function is Public.

8. Create a TwiML Bin: TwiML is an XML document that allows us to programmatically define what the automated voice says when a user picks up our notification phone call.

The Twilio Voice API requires a TwiML resource as part of the call API. Note the url parameter.

Twilio offers an easy way to host your XML document, using TwiML bins. Create a TwiML bin and use the template in the sub-project as an example of what you could have the voice say.

You can customize the voice that speaks by choosing one of the Amazon Polly voices supported.

Once you save your TwiML Bin, you’ll want to take note of the bin’s SID. This will be the value of your function’s TWILIO_TWIML_BIN_ID environment variable.

9. Test out your function! You can use a cURL command to trigger the function and it should respond with a default “Success” message.

$ curl --location --request GET 'https://my-project.twil.io/abcdefghi12345678'
Success%

At this point, you’re all set up to trigger your Twilio function to run a check against vaccine availabilities and if there are openings, it will automatically trigger a phone call and/or e-mail to notify you immediately.

We want to be able to run this check on a recurring basis so that we can constantly monitor for updates. This is where cronhooks comes in. We can use a cron job to call our endpoint again and again, as defined by a schedule.

  1. Sign up for an account on cronhooks.

2. Upgrade to a Basic plan to be able to schedule a recurring call to our function. (There’s a 7 day free trial if you just want to experiment)

3. Create a Recurring Schedule

Your Schedule’s URL will be the Twilio function endpoint. Ensure your Trigger is Recurring.

Use crontab.guru to generate a cron schedule that works for you. In this example, I am using */15 7–23 * * * which means “At every 15th minute past every hour from 7 through 23.” (in ET time zone in this case per the Timezone field). Essentially, every 15 minutes from 7am to midnight.

Take note of DST offset. Consider using UTC time for clarity and consistency and update if your DST offset changes.

Once you’ve scheduled your Webhook, you can click Trigger Now to test that it works.

(Note: After clicking Trigger Now, you will only get notified if there are real availabilities. You can change the DEBUG_MODE environment variable in your function to true and then use Trigger Now to send you a notification immediately even if nothing is actually available. Remember to change it back to false!)

Using these cloud-based solutions, it would cost around $6/month to run:

  • $3/month cronhooks.io Basic Plan
  • $1/month Twilio Phone Number
  • ~$2/month Twilio Runtime & Twilio Voice Pay-as-you-go (depends on your usage levels)

Free alternative solutions:

  • Phone Number: There isn’t an easy way out of this. In order to dial phone calls, we need to rely on a service like Twilio to dispatch calls. If you want to use e-mail only and anticipate <100 emails per day, SendGrid offers a completely free solution.
  • Cron Job: A process needs to trigger the webhook on a recurring basis. You could write a simple looping script or have a cron job that runs on your machine locally that would make the API calls. It would require your machine to be on and running throughout the day, but would be “free”!
  • Serverless Function: We rely on a service like Twilio (could be AWS Lambda + CloudWatch) to host an endpoint that can run the script. Alternatively, you could just run this function locally on a local Node webserver. The quickest way to set this up would just be to run the Twilio dev server (refer to project README) as this will spin up a local webserver and you would just need to have a local service make API calls to http://localhost:3000/nyc-notify to trigger your function.

Software Engineer @ VMware