What and Why?

The Government of India has setup a nationwide COVID-19 vaccination appointment booking system via Co-WIN. There’s an app, a website, and API access too for checking slot availability, getting beneficiary details, appointment booking and also for downloading vaccination certificates.

Vaccination started in January this year. However, vaccination for all residents over the age of 18 only commenced on the 1st of May. With sparse availability of hospitals and institutions that could participate in vaccination for 18+, appointment availability is a challenge. With a lot of slot trackers popping up everyday, I decided to try setting my own tracker up, as this could provide a good learning experience on working with APIs.


API Setu: Co-Win APIs

Heading over to the Co-WIN API documentation on the API Setu portal, there are Public and Protected APIs.

The Public APIs provide the following:

  • Metadata (information on states and districts)
  • Appointment availability
  • Ability to download certificates

The Protected APIs allow booking of vaccination appointments and beneficiary management in addition to the above. However, access is restricted and the API Key required is only provided to partner applications.

For our use case, the public API should suffice.


Getting list of States and Districts

Note: As of writing this, the API isn’t servicing requests unless the ‘User-Agent’ header is set to emulate a browser. It could be possibly due to excessive traffic, for which API requests only from browsers are being allowed.

Starting from the API call to get a list of states,

>>> import requests

>>> request_headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'
}

>>> response = requests.get(
    "https://cdn-api.co-vin.in/api/v2/admin/location/states",
    headers=request_headers)

This gives the following output:

>>> response.json()
{'states': [{'state_id': 1, 'state_name': 'Andaman and Nicobar Islands'}, {'state_id': 2, 'state_name': 'Andhra Pradesh'}, {'state_id': 3, 'state_name': 'Arunachal Pradesh'}, {'state_id': 4, 'state_name': 'Assam'}, {'state_id': 5, 'state_name': 'Bihar'}, {'state_id': 6, 'state_name': 'Chandigarh'}, {'state_id': 7, 'state_name': 'Chhattisgarh'}, {'state_id': 8, 'state_name': 'Dadra and Nagar Haveli'}, {'state_id': 37, 'state_name': 'Daman and Diu'}, {'state_id': 9, 'state_name': 'Delhi'}, {'state_id': 10, 'state_name': 'Goa'}, {'state_id': 11, 'state_name': 'Gujarat'}, {'state_id': 12, 'state_name': 'Haryana'}, {'state_id': 13, 'state_name': 'Himachal Pradesh'}, {'state_id': 14, 'state_name': 'Jammu and Kashmir'}, {'state_id': 15, 'state_name': 'Jharkhand'}, {'state_id': 16, 'state_name': 'Karnataka'}, {'state_id': 17, 'state_name': 'Kerala'}, {'state_id': 18, 'state_name': 'Ladakh'}, {'state_id': 19, 'state_name': 'Lakshadweep'}, {'state_id': 20, 'state_name': 'Madhya Pradesh'}, {'state_id': 21, 'state_name': 'Maharashtra'}, {'state_id': 22, 'state_name': 'Manipur'}, {'state_id': 23, 'state_name': 'Meghalaya'}, {'state_id': 24, 'state_name': 'Mizoram'}, {'state_id': 25, 'state_name': 'Nagaland'}, {'state_id': 26, 'state_name': 'Odisha'}, {'state_id': 27, 'state_name': 'Puducherry'}, {'state_id': 28, 'state_name': 'Punjab'}, {'state_id': 29, 'state_name': 'Rajasthan'}, {'state_id': 30, 'state_name': 'Sikkim'}, {'state_id': 31, 'state_name': 'Tamil Nadu'}, {'state_id': 32, 'state_name': 'Telangana'}, {'state_id': 33, 'state_name': 'Tripura'}, {'state_id': 34, 'state_name': 'Uttar Pradesh'}, {'state_id': 35, 'state_name': 'Uttarakhand'}, {'state_id': 36, 'state_name': 'West Bengal'}], 'ttl': 24}

Let’s pick Maharashtra to start with. From the response above, we see **{'state_id': 9, 'state_name': 'Delhi'}**


To list districts for a given state,

>>> state_code = 9  # For Delhi

>>> response = requests.get(
    f"https://cdn-api.co-vin.in/api/v2/admin/location/districts/{state_code}",
    headers=request_headers)

>>> response.json()
{'districts': [{'district_id': 141, 'district_name': 'Central Delhi'}, {'district_id': 145, 'district_name': 'East Delhi'}, {'district_id': 140, 'district_name': 'New Delhi'}, {'district_id': 146, 'district_name': 'North Delhi'}, {'district_id': 147, 'district_name': 'North East Delhi'}, {'district_id': 143, 'district_name': 'North West Delhi'}, {'district_id': 148, 'district_name': 'Shahdara'}, {'district_id': 149, 'district_name': 'South Delhi'}, {'district_id': 144, 'district_name': 'South East Delhi'}, {'district_id': 150, 'district_name': 'South West Delhi'}, {'district_id': 142, 'district_name': 'West Delhi'}], 'ttl': 24}

Taking the first district, Central Delhi, we see: {'district_id': 141, 'district_name': 'Central Delhi'}


Getting Appointment information

There are multiple ways to get information for appointments: by PIN code, by Lat/Long, or by district. I’ll be using the district_id above to find appointments.

There’s an option to either use the /findByDistrict, or the /calendarByDistrict endpoint. I’ll be using the latter to get session information for 7 days, given a district.

>>> date = "15-05-2021"
>>> district_code = "141"


>>> response = requests.get(
    f"https://cdn-api.co-vin.in/api/v2/appointment/sessions/public/calendarByDistrict?district_id={district_code}&date={date}",
    headers=request_headers
)
{'centers': [{'center_id': 632898, 'name': 'DGD Tis Hazari', 'address': 'Tis Hazari Court', 'state_name': 'Delhi', 'district_name': 'Central Delhi', 'block_name': 'Not Applicable', 'pincode': 110054, 'lat': 28, 'long': 77, 'from': '09:00:00', 'to': '17:00:00', 'fee_type': 'Free', 'sessions': [{'session_id': '49747703-7915-4282-931c-3241aee8e84e', 'date': '15-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '663ea8a2-5673-4785-b696-34eb9af10224', 'date': '17-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '560ada0e-d8f1-46a7-9bcb-0764f852bd6f', 'date': '18-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '0da3ad4b-7a45-4838-bd94-5ed2d1163ba7', 'date': '19-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': 'cbd47fe6-858a-48f3-bf07-74474b92f4cd', 'date': '20-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '687b6cdc-6878-46e5-8cb6-c08c2b3dc4f5', 'date': '21-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}]},
<<<<<<Snipping some data out>>>>>>>>
{'center_id': 560735, 'name': 'Burari Hospital Session Site 1', 'address': 'Kaushik Enclave, Shankarpura, Burari', 'state_name': 'Delhi', 'district_name': 'Central Delhi', 'block_name': 'Not Applicable', 'pincode': 110084, 'lat': 28, 'long': 77, 'from': '09:00:00', 'to': '18:00:00', 'fee_type': 'Free', 'sessions': [{'session_id': 'f317c15b-80f3-4e8d-8e3d-3925f5495400', 'date': '15-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-06:00PM']}, {'session_id': '085503a5-e378-4d9c-99b6-406d20705a2d', 'date': '16-05-2021', 'available_capacity': 1, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': 'a89d4468-8014-4426-8625-6df9f2006d99', 'date': '17-05-2021', 'available_capacity': 134, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '1afbed58-a1a0-4e94-b8ad-a8c5e0ef8ec1', 'date': '18-05-2021', 'available_capacity': 143, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '6ed55c41-681e-4793-a113-2d6048210b81', 'date': '19-05-2021', 'available_capacity': 176, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': 'cf2b702a-d513-4480-8434-dc3df77dad8a', 'date': '20-05-2021', 'available_capacity': 152, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '8bb91089-3175-47f9-a882-cfcf81e0d6af', 'date': '21-05-2021', 'available_capacity': 177, 'min_age_limit': 45, 'vaccine': 'COVISHIELD', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}]}]}

The response is a long JSON output, which is structured as below:

Key called centers –> Each center contains further information as:

And again, each session can be split as:

To see data for one such center,

>>> response.json()['centers'][0]
{'center_id': 632898, 'name': 'DGD Tis Hazari', 'address': 'Tis Hazari Court', 'state_name': 'Delhi', 'district_name': 'Central Delhi', 'block_name': 'Not Applicable', 'pincode': 110054, 'lat': 28, 'long': 77, 'from': '09:00:00', 'to': '17:00:00', 'fee_type': 'Free', 'sessions': [{'session_id': '49747703-7915-4282-931c-3241aee8e84e', 'date': '15-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '663ea8a2-5673-4785-b696-34eb9af10224', 'date': '17-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '560ada0e-d8f1-46a7-9bcb-0764f852bd6f', 'date': '18-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '0da3ad4b-7a45-4838-bd94-5ed2d1163ba7', 'date': '19-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': 'cbd47fe6-858a-48f3-bf07-74474b92f4cd', 'date': '20-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}, {'session_id': '687b6cdc-6878-46e5-8cb6-c08c2b3dc4f5', 'date': '21-05-2021', 'available_capacity': 0, 'min_age_limit': 45, 'vaccine': 'COVAXIN', 'slots': ['09:00AM-11:00AM', '11:00AM-01:00PM', '01:00PM-03:00PM', '03:00PM-05:00PM']}]}

Prettifying the above JSON data:

{
   "center_id":632898,
   "name":"DGD Tis Hazari",
   "address":"Tis Hazari Court",
   "state_name":"Delhi",
   "district_name":"Central Delhi",
   "block_name":"Not Applicable",
   "pincode":110054,
   "lat":28,
   "long":77,
   "from":"09:00:00",
   "to":"17:00:00",
   "fee_type":"Free",
   "sessions":[
      {
         "session_id":"49747703-7915-4282-931c-3241aee8e84e",
         "date":"15-05-2021",
         "available_capacity":0,
         "min_age_limit":45,
         "vaccine":"COVAXIN",
         "slots":[
            "09:00AM-11:00AM",
            "11:00AM-01:00PM",
            "01:00PM-03:00PM",
            "03:00PM-05:00PM"
         ]
      },
      {
         "session_id":"663ea8a2-5673-4785-b696-34eb9af10224",
         "date":"17-05-2021",
         "available_capacity":0,
         "min_age_limit":45,
         "vaccine":"COVAXIN",
         "slots":[
            "09:00AM-11:00AM",
            "11:00AM-01:00PM",
            "01:00PM-03:00PM",
            "03:00PM-05:00PM"
         ]
      },
      {
         "session_id":"560ada0e-d8f1-46a7-9bcb-0764f852bd6f",
         "date":"18-05-2021",
         "available_capacity":0,
         "min_age_limit":45,
         "vaccine":"COVAXIN",
         "slots":[
            "09:00AM-11:00AM",
            "11:00AM-01:00PM",
            "01:00PM-03:00PM",
            "03:00PM-05:00PM"
         ]
      },
      {
         "session_id":"0da3ad4b-7a45-4838-bd94-5ed2d1163ba7",
         "date":"19-05-2021",
         "available_capacity":0,
         "min_age_limit":45,
         "vaccine":"COVAXIN",
         "slots":[
            "09:00AM-11:00AM",
            "11:00AM-01:00PM",
            "01:00PM-03:00PM",
            "03:00PM-05:00PM"
         ]
      },
      {
         "session_id":"cbd47fe6-858a-48f3-bf07-74474b92f4cd",
         "date":"20-05-2021",
         "available_capacity":0,
         "min_age_limit":45,
         "vaccine":"COVAXIN",
         "slots":[
            "09:00AM-11:00AM",
            "11:00AM-01:00PM",
            "01:00PM-03:00PM",
            "03:00PM-05:00PM"
         ]
      },
      {
         "session_id":"687b6cdc-6878-46e5-8cb6-c08c2b3dc4f5",
         "date":"21-05-2021",
         "available_capacity":0,
         "min_age_limit":45,
         "vaccine":"COVAXIN",
         "slots":[
            "09:00AM-11:00AM",
            "11:00AM-01:00PM",
            "01:00PM-03:00PM",
            "03:00PM-05:00PM"
         ]
      }
   ]
}

So now effectively what needs to be done, is to find sessions with "min_age_limit":45, or "min_age_limit":18 (as the case may be), and having "available_capacity" > 0.

Looping through the session data across our result for the above two conditions gives us:

>>> for center in response.json()["centers"]:
        for session in center['sessions']:
            if session['min_age_limit'] == 45 and session['available_capacity'] > 0:
                print(f"Found a slot, with details: \n"
                      f" center name: {center['name']} \n"
                      f" center PIN Code: {center['pincode']} \n"
                      f" date: {session['date']} \n"
                      f" available capacity: {session['available_capacity']}")

Found a slot, with details:
 center name: MAMC SITE 1
 center PIN Code: 110006
 date: 18-05-2021
 available capacity: 118
Found a slot, with details:
 center name: MAMC SITE 1
 center PIN Code: 110006
 date: 19-05-2021
 available capacity: 170
Found a slot, with details:
 center name: MAMC SITE 1
 center PIN Code: 110006
 date: 20-05-2021
 available capacity: 168
Found a slot, with details:
 center name: MAMC SITE 1
 center PIN Code: 110006
 date: 21-05-2021
 available capacity: 163
Found a slot, with details:
 center name: Hindu Rao Hospital SITE 6
 center PIN Code: 110007
 date: 15-05-2021
 available capacity: 1
Found a slot, with details:
 center name: Hindu Rao Hospital SITE 6
 center PIN Code: 110007
 date: 17-05-2021
 available capacity: 169
Found a slot, with details:
 center name: Hindu Rao Hospital SITE 6
 center PIN Code: 110007
 date: 18-05-2021
 available capacity: 194

>>> # Lot of slots available.

Trying for the 18-44 category though:

>>> for center in response.json()["centers"]:
        for session in center['sessions']:
            if session['min_age_limit'] == 18 and session['available_capacity'] > 0:
                print(f"Found a slot, with details: \n"
                      f" center name: {center['name']} \n"
                      f" center PIN Code: {center['pincode']} \n"
                      f" date: {session['date']} \n"
                      f" available capacity: {session['available_capacity']}")

>>> # No results

Setting up Telegram Alerting

Telegram has an extensive API for making bots, which is thoroughly documented here. On completing the steps below, you can send Telegram messages to a user by simply making a GET request to the Telegram API.

The main steps involved are: (explained in detail here)

  • Getting a bot token,

  • Getting your chat_id,

  • Making a GET request to alert.

  1. Getting a bot token

The first step involves searching for the BotFather.

Send /newbot to the BotFather, set a name for it, and choose a username. Make sure it ends with ‘bot’. It may take some tries if the username is already taken.

Once successful, note the token and keep it safe. We’ll need it in the next step. Open the t.me/co_win_slot_bot link to get the chat window, and send any message to the bot. I have just sent the default START message.

  1. Getting your chat_id

The chat_id parameter is essentially an alias to your Telegram username. It can be used to address someone and send messages to them.

Since we just sent the /start message to the bot, we can retrieve details of the message as received by the bot, and get our chat_id. To retrieve said details, all that’s needed is a getUpdates call to the Telegram API.

Just visit https://api.telegram.org/bot__XXX:YYYYY__/getUpdates (replace the XXX: YYYYY with your BOT HTTP API Token you just got from the Telegram BotFather)

{
  "ok":true,
  "result":[
    {
      "update_id":856479972,
      "message":{
        "message_id":6216,
        "from":{
          "id":<<chat_id here>>,
          "is_bot":false,
          "first_name":"Siddhant",
          "last_name":"Shah",
          "username":"<<your username>>",
          "language_code":"en"
        },
        "chat":{
          "id":<<chat_id here>>,
          "first_name":"Siddhant",
          "last_name":"Shah",
          "username":"<<your username>>",
          "type":"private"
        },
        "date":1620907696,
        "text":"/start",
        "entities":[
          {
            "offset":0,
            "length":6,
            "type":"bot_command"
          }
        ]
      }
    }
  ]
}

Your chat_id can be found in the "id":<<chat_id here>> line from the response received.

  1. Making a GET request to alert

To send a message msg to a user with chat_id as Chat ID, a simple GET request is to be made in the following format:

https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}&text={msg}

To send a simple “Hello” as a message, we have to:

>>> import requests
>>> token = "bot token here"
>>> chat_id = "your chat_id goes here"
>>> msg = "Hello!"
>>> r = requests.get(f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}&text={msg}")

Great! So all we need to do now is to keep checking for any slot to open, and if there’s any appointment available, send the details as msg in the telegram message as above.


Combining all, we get something like this:

import requests

request_headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'
}



def alert_telegram(msg):
    token = "bot token here"
    chat_id = "your chat_id goes here"
    r = requests.get(f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}&text={msg}")


date = "15-05-2021"
district_code = "141"
min_age = 45

response = requests.get(
    f"https://cdn-api.co-vin.in/api/v2/appointment/sessions/public/calendarByDistrict?district_id={district_code}&date={date}",
    headers=request_headers)
for center in response.json()["centers"]:
        for session in center['sessions']:
            if session['min_age_limit'] == min_age and session['available_capacity'] > 0:
                message = (f"Found a slot, with details: \n"
                      f" center name: {center['name']} \n"
                      f" center PIN Code: {center['pincode']} \n"
                      f" date: {session['date']} \n"
                      f" available capacity: {session['available_capacity']}")
                alert_telegram(message)

This gives a lot of results 😂.

Changing the min_age = 18 gives none, as at that moment, there weren’t any such slots available.

To get notified of any available vaccination appointment or slots, we can run the code above and be notified on Telegram for the same.


To make the script run every X minutes

To do so, either:

  • Put the above code in an indefinite loop with some sleep().

  • Set it to run as a cron job (preferred)

  1. With an indefinite loop with sleep()
import requests
import time

request_headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'
}



def alert_telegram(msg):
    token = "bot token here"
    chat_id = "your chat_id goes here"
    r = requests.get(f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}&text={msg}")


date = "15-05-2021"
district_code = "141"
min_age = 45


while True:
    response = requests.get(
        f"https://cdn-api.co-vin.in/api/v2/appointment/sessions/public/calendarByDistrict?district_id={district_code}&date={date}",
        headers=request_headers)
    for center in response.json()["centers"]:
            for session in center['sessions']:
                if session['min_age_limit'] == min_age and session['available_capacity'] > 0:
                    message = (f"Found a slot, with details: \n"
                          f" center name: {center['name']} \n"
                          f" center PIN Code: {center['pincode']} \n"
                          f" date: {session['date']} \n"
                          f" available capacity: {session['available_capacity']}")
                    alert_telegram(message)
    time.sleep(300)

time.sleep() has been added for 300 seconds, or 5 minutes. This isn’t recommended for a variety of reasons, some of which are described here.

  1. Set to run the script as a cron-job (UNIX based) or a Windows Scheduled Task

For Linux users:

ubuntu@siddhant:~$ which python3
/usr/bin/python3
# This was done to get the python PATH.

ubuntu@siddhant:~$ crontab -e

Select an editor here, and add the following to the bottom of the file:

*/5 * * * * /usr/bin/python3 <path to your python script> >/dev/null 2>&1

This will make the script run every 5 minutes. More help in making your own cronjobs can be found here. Also, crontab.guru is a great resource for making your own expressions.

Windows users can set up a scheduled task to run every X minutes, which is explained here.

Note: Please be aware that the Co-WIN API has a rate limit of 100 API calls per 5 minutes per IP. So please pace requests to the API accordingly.