Sending app notifications over Whatsapp using Twilio
Tags: twilio, python, whatsapp
Published: 2020-10-22 00:46:17

---
title: Sending app notifications over Whatsapp using Twilio
published: true
description: 
tags: #twilio,#python,#whatsapp
---

**Introduction**

In this article, we are going to build an application that sends appointment notifications to users over whatsapp via twilio. According to statistics, 90.4% of the young generation are active on social media so this approach of sending notifications will be more effective in setting remiders without being intrusive.

**User Journey**

An API request is sent to the application containing the appointment details (title, description, phone and time). Once the appointment time is reached, the user gets notified on whatsapp about the appointment. 

![](https://paper-attachments.dropbox.com/s_84F97AD51A16E7A0A60C1D1E89892F9FB45EDB6B9586E29DC715D4180B666E28_1582478259744_Screen+Shot+2020-02-23+at+6.10.33+PM.png)

**Tutorial Requirements**

To follow this tutorial, you are expected to:

- Have sufficient understanding of Python and Flask
- Have Python 3 installed on your machine
- Have MongoDB installed on your machine
- Have a smartphone with an active phone number and Whatsapp installed
- Have a Twilio account. You can create a free account if you are new to Twilio.


**Setting up the Twilio Whatsapp Sandbox**

First of all, you need to create a Twilio account if you don’t have one or sign in if you have one. You will need to activate Twilio Whatsapp sandbox since we are going to be working with Whatsapp. The sandbox allows you to immediately test with Whatsapp using a phone number without waiting for you Twilio number to be approved by Whatsapp. You can [request production access for your Twilio phone number here](https://www.twilio.com/whatsapp/request-access). 
To connect to the sandbox, log into your [Twilio Console](https://www.twilio.com/console) and select [Programmable SMS](https://www.twilio.com/console/sms/dashboard) on the side menu. After that, click on [Whatsapp](https://www.twilio.com/console/sms/whatsapp/learn). This will open the Whatsapp sandbox page which contains your sandbox phone number and a join code. Send the join code starting with the word “join” to your sandbox phone number to enable the Whatsapp sandbox for your phone. You will then receive a message confirming the activation of the phone number to use the sandbox. 

**Application Setup**

We are going to be using the Flask framework to build the application and MongoDB as our preferred database. 

Creating the Application Directory and Virtual Environment
Run the following commands in your terminal to create the project folder and virtual environment for this project.

- Create project directory named `whatsapp_appointments`
    mkdir whatsapp_appointments
- Enter into the project directory
    cd whatsapp_appointments
- Create a virtual environment for the project
    python3 -m venv venv
- Activate the virtual environment

For macOS and Linux users:

    source venv/bin/activate

For windows users:

    venv\Scripts\activate

The virtual environment helps create an isolated environment separate from the global python environment to run the project.

**Structuring the Project**
In the application directory, run the commands below to set up the folder structure:

    mkdir controllers jobs services utils 
    touch app.py

The above commands create 4 folders and a file called `app.py`  in the application directory.
Your application directory should look like this:

![](https://paper-attachments.dropbox.com/s_84F97AD51A16E7A0A60C1D1E89892F9FB45EDB6B9586E29DC715D4180B666E28_1582463434802_Screen+Shot+2020-02-23+at+2.10.14+PM.png)


**Installing Project Dependencies**
Finally, let’s install all the dependencies used in this project.

- Flask: This library will be used for running our web server.
- Pymongo: This library will be used to interface with the MongoDB database on your computer.
- Twilio Python Helper Library: This library will be used to send Whatsapp messages to users.

Run the command below to install these dependencies:

    pip install Flask pymongo twilio
**Building appointment reminders**

In this section, we are going to write the logic for appointment reminders. We are going to use the `mutiprocessing` module in python to build the reminders logic. The main reason we are using processes over threads in this section is because in python, there is no safe way to terminate threads and we will need to edit and cancel appointments 🤷🏿‍♂️.
First of all, create 2 files called `__init__.py` and `appointments.py` in the `jobs/` directory. Also, create a file called `whatsapp.py` in the `utils/` directory. 
Copy the code below into the `whatsapp.py` file:

    import os
    from twilio.rest import Client
    
    ACCOUNT_SID = os.getenv('ACCOUNT_SID')
    AUTH_TOKEN = os.getenv('AUTH_TOKEN')
    TWILIO_NUMBER = os.getenv('TWILIO_NUMBER')
    client = Client(ACCOUNT_SID, AUTH_TOKEN)
    
    
    def format_message(appointment):
        message = "You have an appointment\n"
        message += f"Title: {appointment['title']}\n"
        message += f"Description: {appointment['description']}\n"
        message += f"Time: {appointment['time']}"
        return message
    
    
    def send_message(appointment):
        message = client.messages.create(
            from_=f'whatsapp:{TWILIO_NUMBER}',
            body=format_message(appointment),
            to=f"whatsapp:{appointment['phone']}"
        )
        return message.sid

In the above code, we have the logic for sending whatsapp messages. Note that the `ACCOUNT_SID`, `AUTH_TOKEN`, `TWILIO_NUMBER` are set as environment variables and we use `os.getenv` to access them. To send whatsapp messages, you must prefix the phone numbers with `whatsapp:` else it will be sent as sms. The `format_message` function accepts an appointment object and represents it in the following format:

    You have an appointment
    Title: <Appointment Title>
    Description: <Appointment Description>
    Time: <Appointment Time>

Next, we are going to write the reminder’s logic. Open the file `jobs/appointments.py` and copy the following code inside:

    import time
    from datetime import datetime
    
    from utils.whatsapp import send_message
    
    
    def start_appointment_job(appointment):
        print(f"=====> Scheduled Appointment {appointment['_id']} for :> {appointment['time']}")
        if datetime.now() > appointment['time']:
            return
        diff = appointment['time'] - datetime.now()
        time.sleep(diff.seconds)
        send_message(appointment)
    

The `start_appointment_job` function first checks if the appointment is in the past and if so, it returns. We use the `time.sleep` function to tell the application to pause execution for the number of seconds it takes for the appointment’s time to be reached. Once it finishes sleeping, it calls the `send_message` function written earlier to notify the user of the appointment via whatsapp.
Next, we are going to write the service functions for creating, getting, updating and deleting appointments. Create a file called `appointments.py` in the `services/` folder and copy the following inside:

    from bson.objectid import ObjectId
    from pymongo import MongoClient
    
    client = MongoClient('mongodb://localhost/')
    
    appointments = client['twilio']['appointments']
    
    
    def create_appointment(data):
        appointments.insert_one(data)
    
    
    def get_appointment(appointment_id):
        result = appointments.find_one({'_id': ObjectId(appointment_id)})
        return dict(result) if result else None
    
    
    def get_appointments(conditions):
        if '_id' in conditions:
            conditions['_id'] = ObjectId(conditions['_id'])
        results = appointments.find(conditions)
        data = []
        for result in results:
            data.append(dict(result))
        return data
    
    
    def update_appointment(appointment_id, data):
        appointment = appointments.find_one_and_update({'_id': ObjectId(appointment_id)}, {'$set': data},
                                                       return_document=True)
        return appointment
    
    
    def delete_appointment(appointment_id):
        appointments.find_one_and_delete({'_id': ObjectId(appointment_id)})
    

In the above code, we write functions to perform CRUD operations on the `appointments` collections in mongodb. We use the `pymongo` library to connect to our local mongodb instance. Note that the `_id` field cannot be sent as a string else no data will be returned, we have to cast it to an `ObjectId` instance which mongodb uses. 

Finally, we are going to write the logic for creating processes for an appointment and modifying them based on the change in appointments. Open `jobs/__init__.py` and copy the following code inside:

    from multiprocessing import Process
    from datetime import datetime
    
    from .appointments import start_appointment_job
    
    from services.appointments import get_appointments
    
    WORKERS = {}
    
    
    def terminate_worker(worker):
        try:
            worker.terminate()
            worker.join()
            worker.close()
        except Exception as err:
            print('====> Error occurred terminating process', err)
    
    
    def schedule_appointment(appointment):
        appointment_id = str(appointment['_id'])
        worker = Process(target=start_appointment_job, args=(appointment,))
        worker.start()
        WORKERS[appointment_id] = worker
    
    
    def update_scheduled_appointment(appointment_id, updated_appt):
        worker = WORKERS[appointment_id]
        terminate_worker(worker)
        new_worker = Process(target=start_appointment_job, args=(updated_appt,))
        new_worker.start()
        WORKERS[appointment_id] = new_worker
    
    
    def delete_scheduled_appointment(appointment_id):
        worker = WORKERS[appointment_id]
        terminate_worker(worker)
        del WORKERS[appointment_id]
    
    def init_workers():
        print('=====> Initializing workers')
        appts = get_appointments({})
        for appt in appts:
            if datetime.now() > appt['time']:
                continue
            schedule_appointment(appt)
    
    def close_workers():
        for appointment_id, worker in WORKERS.items():
            terminate_worker(worker)
    

In the above code, when we want to schedule an appointment, we create a `Process` targeting the `start_appointment_job` function with the `appointment` object as argument. Note that we have a dictionary objection named `WORKERS` where we store the process object with the `appointment_id` as key, this is what we use to get the `Process` object in case of an update. 
To update an appointment, we get the process object via the `appointment_id` and terminate it. We then create a new `Process` object with the updated details and update the reference in the `WORKERS` dictionary. 
To delete an appointment, we get the `Process` object and terminate it. We then delete the key from the `WORKERS` dictionary so that it doesn’t have a reference there again. 
Note that, to terminate a `Process` object, we first call the `terminate` function on it when sends a `SIGTERM` signal to the process. The `join` function waits for the `Process` object to terminate and the `close` function tells the process to release the resources that it holds. 
Lets say our application restarts, we want to be able to recreate our Process objects and that’s where the `init_workers` function comes into play. It gets all the appointments and calls the `schedule_appointment` function on appointments that are still pending.


**Building the API**

In this section, we are going to be building the api routes which we are going use to create, update, get and delete appointments. First of all, we have to create utility functions for handling both our requests and our api responses. Create 2 files called `request.py` and `response.py` in the `utils/` folder. Copy the code below into the `request.py` file:


    from datetime import datetime
    
    
    def validate_body(data, required_fields):
        for field in required_fields:
            if field not in data:
                return False, field
        return True, None
    
    
    def parse_appointment(body):
        try:
            if body.get('time'):
                time_obj = datetime.strptime(body['time'], '%Y-%m-%d %H:%M')
                body['time'] = time_obj
            return True, None
        except Exception as err:
            return False, str(err)
    

From the above code, we can see that the `validate_body` function takes in a data dictionary and a list of required fields. It checks the required fields against the data dictionary and returns a status of False if a field is absent. The `parse_appointment` function is used to convert the `time` field in an appointment object into a datetime object by the use of `datetime.strptime` function which accepts the time string and the time format.  The function returns false if it’s not able to convert the string into a datetime object. 
Next, copy the code below into the `response.py` file:

    import json
    import time
    
    from flask import jsonify
    
    
    def stringify_objectid(data):
        str_data = json.dumps(data, default=str)
        return json.loads(str_data)
    
    
    def response(status, message, data, status_code=200):
        """
        :param status : Boolean Status of the request
        :param status_code: Status Code of response
        :param message : String message to be sent out as description of the message
        :param data : dictionary representing extra data
        """
        if data:
            data = stringify_objectid(data)
        res = {'status': status, 'message': message, 'data': data, 'timestamp': timestamp()}
        return jsonify(res), status_code
    
    
    def error_response(message, status='error', code='R0', status_code=400):
        res = {'message': message, 'status': status, 'code': code}
        return jsonify(res), status_code
    
    
    def timestamp():
        """
        Helper Function to generate the current time
        """
        return time.time()
    

In the above code, we have helper functions to return api responses and error messages. The `stringify_objectid` function is used to convert the mongodb ObjectId object into a string.

Next, we are going to write our controller functions. Create a file called `appointments.py` in the `controllers/` folder and copy the following inside:

    from flask import Blueprint, request
    
    from services import appointments as apt_service
    from utils.request import validate_body, parse_appointment
    from utils.response import response, error_response
    from utils.whatsapp import send_sms
    
    import jobs
    
    appointments = Blueprint('appointments', __name__)
    
    
    @appointments.route('/', methods=['POST'])
    def create():
        body = request.get_json()
        status, missing_field = validate_body(body, ['title', 'phone', 'description', 'time'])
        if not status:
            return error_response(f'{missing_field} is required')
        status, error = parse_appointment(body)
        if not status:
            return error_response(error)
        try:
            apt_service.create_appointment(body)
            jobs.schedule_appointment(body)
            return response(True, 'Appointment created successfully', body)
        except Exception as err:
            print('=====> Error', err)
            return error_response(str(err))
    
    
    @appointments.route('/')
    def view():
        conditions = dict(request.args)
        try:
            data = apt_service.get_appointments(conditions)
            return response(True, 'Appointments', data)
        except Exception as err:
            print('=====> Error', err)
            return error_response(str(err))
    
    
    @appointments.route('/<appointment_id>')
    def view_one(appointment_id):
        try:
            data = apt_service.get_appointment(appointment_id)
            return response(True, 'Appointment', data)
        except Exception as err:
            print('=====> Error', err)
            return error_response(str(err))
    
    @appointments.route('/<appointment_id>', methods=['PUT'])
    def update(appointment_id):
        body = request.get_json()
        try:
            parse_appointment(body)
            appointment = apt_service.update_appointment(appointment_id, body)
            jobs.update_scheduled_appointment(appointment_id, appointment)
            return response(True, 'Updated Appointment', appointment)
        except Exception as err:
            print('=====> Error', err)
            return error_response(str(err))
    
    
    @appointments.route('/<appointment_id>', methods=['DELETE'])
    def delete(appointment_id):
        try:
            apt_service.delete_appointment(appointment_id)
            jobs.delete_scheduled_appointment(appointment_id)
            return response(True, 'Appointment deleted successfully', None)
        except Exception as err:
            print('====> Error', err)
            return error_response(str(err))
    

In the above code, we created a new flask blueprint and functions to create appointments, get appointments, update appointments and delete appointments. As you can see from the above code, we call the schedule appointment function to start the scheduling process when an appointment is created. The update function calls the `update_scheduled_appointment` function to reshedule the appointment and the delete function calls the `delete_scheduled_appointment` function to delete the scheduling process. 

Finally, open the `app.py` file and copy the following inside:

    import atexit
    
    from flask import Flask
    
    from controllers.appointments import appointments
    from jobs import close_workers, init_workers
    
    
    def create_app():
        init_workers()
        atexit.register(close_workers)
        return Flask(__name__)
    
    
    app = create_app()
    
    app.register_blueprint(appointments, url_prefix='/api/appointments')
    
    if __name__ == '__main__':
        app.run()
    

In the above code, the use the `register_blueprint` function to make sure that urls starting  with `/api/appointments` are routed through the appointments blueprint and our controller functions are executed for matching urls. We use the `atexit` module to perform a task when the application is closing. In the `create_app` function, we are telling the system to first schedule pending appointments and then close all running processes when the application is closing. 

**Testing the Application**

To run the application, open your terminal and execute `python app.py` in your application’s directory. You should see something like the image below:

![](https://paper-attachments.dropbox.com/s_84F97AD51A16E7A0A60C1D1E89892F9FB45EDB6B9586E29DC715D4180B666E28_1582477244943_Screen+Shot+2020-02-23+at+6.00.35+PM.png)


Now, you can send a POST request to `http://127.0.0.1:5000/api/appointments` to create an appointment and you will get a whatsapp message when the appointment time has been reached. Below is a sample request body:

    {
            "title": "Data Structures & Algorithms",
            "phone": "+2349094739283",
            "description": "Watch Youtube videos on Data Structures and Algorithms",
            "time":"2020-02-23 18:10"
    }

You should receive a whatsapp message when the appointment time is reached as shown in the image below

![](https://paper-attachments.dropbox.com/s_84F97AD51A16E7A0A60C1D1E89892F9FB45EDB6B9586E29DC715D4180B666E28_1582477884158_Screen+Shot+2020-02-23+at+6.10.33+PM.png)

Conclusion

We have now come to the end of the tutorial. We have successfully built an application that sends appointment notification over Whatsapp using Twilio, Python and Flask. 
You will find the source code for this tutorial [here](https://github.com/Favouroked/twilio_appointments).