How to Receive Images on WhatsApp using Django and Twilio

June 17, 2021
Written by
Reviewed by
Diane Phan
Twilion

How to Receive Images on WhatsApp using Django and Twilio

The WhatsApp Business API from Twilio is a powerful, yet easy to use service that allows you to communicate with your users on the popular messaging app. In this tutorial you are going to learn how to create a Python application based on the Django web framework that can receive and handle images sent by your users on WhatsApp.

Project demo

Prerequisites

To follow this tutorial you need the following items:

The Twilio WhatsApp sandbox

Twilio provides a WhatsApp sandbox, where you can easily develop and test your application. Once your application is complete you can request production access for your Twilio phone number, which requires approval by WhatsApp.

In this section you are going to connect your smartphone to the sandbox. From your Twilio Console, select Messaging, then click on “Try it Out”. Choose the WhatsApp section on the left hand sidebar. The WhatsApp sandbox page will show you the sandbox number assigned to your account, and a join code.

WhatsApp Sandbox configuration

To enable the WhatsApp sandbox for your smartphone, send a WhatsApp message with the given code to the number assigned to your account. The code is going to begin with the word "join", followed by a randomly generated two-word phrase. Shortly after you send the message you should receive a reply from Twilio indicating that your mobile number is connected to the sandbox and can start sending and receiving messages.

If you intend to test your application with additional smartphones, then you must repeat the sandbox registration process with each of them.

Project setup

In this section you are going to set up a brand new Django project. To keep things nicely organized, open a terminal or command prompt, find a suitable place and create a new directory where the project you are about to create will live:

mkdir whatsapp-images
cd whatsapp-images

Creating a virtual environment

Following Python best practices, you are going to create a virtual environment to install the Python dependencies needed for this project.

If you are using a Unix or Mac OS system, open a terminal and enter the following commands to create and activate your virtual environment:

python3 -m venv venv
source venv/bin/activate

If you are following the tutorial on Windows, enter the following commands in a command prompt window:

python -m venv venv
venv\Scripts\activate

Now you are ready to install the Python dependencies used by this project:

pip install django twilio requests pyngrok

The four Python packages that are needed by this project are:

  • The Django framework, to create the web application.
  • The Twilio Python Helper library, to work with WhatsApp messages.
  • The requests package, to download the images that users send over WhatsApp.
  • Pyngrok, to make the Django application temporarily accessible on the Internet for testing via the ngrok utility.

Creating a Django project

In this step you are going to create a brand new Django web application. Enter the following commands in the same terminal you used to create and activate the virtual environment:

django-admin startproject images .
django-admin startapp whatsapp
python manage.py migrate
python manage.py runserver

The first command above creates a Django project called images. You will see a subdirectory with that name created in the top-level directory of your project. The next command defines a Django application called whatsapp. After you run this second command you will also see a subdirectory with that name added to the project. This is the application in which you will build the logic to converse with users over WhatsApp.

The migrate command performs the default Django database migrations, which are necessary to fully set up the Django project. The runserver command starts the Django development web server.

In general you will want to leave the Django web server running while you write code, because it automatically detects code changes and restarts to incorporate them. So leave this terminal window alone and open a second terminal to continue the tutorial.

Starting an ngrok tunnel

The Django web server is only available locally inside your computer, which means that it cannot be accessed over the Internet. To be able to connect with WhatsApp, Twilio needs to be able to send web requests to this server. Thus during development, a trick is necessary to make the local server available on the Internet.

On your second terminal window, activate the virtual environment and then run the following command:

ngrok http 8000

The ngrok screen should look as follows:

ngrok

Note the https:// forwarding URL. This URL is temporarily mapped to your Django web server, and can be accessed from anywhere in the world. Any requests that arrive on it will be transparently forwarded to your server by the ngrok service. The URL is active for as long as you keep ngrok running, or until the ngrok session expires. Each time ngrok is launched a new randomly generated URL will be mapped to the local server.

It is highly recommended that you create a free Ngrok account and install your Ngrok account's authtoken on your computer to avoid hitting limitations in this service. See this blog post for details.

Open the file settings.py from the images directory in your text editor or IDE. Find the line that has the ALLOWED_HOSTS variable and change it as follows:

ALLOWED_HOSTS = ['.ngrok.io']

This will tell Django that requests received from ngrok URLs are allowed.

While still running the Django server and ngrok on two separate terminals, type https://xxxxxx.ngrok.io on the address bar of your web browser to confirm that your Django project is up and running. Replace xxxxx with the randomly generated subdomain from your ngrok session. This is what you should see:

Django server

Leave the Django server and ngrok running while you continue working on the tutorial. If your ngrok session expires, stop it by pressing Ctrl-C, and start it again to begin a new session. Remember that each time you restart ngrok the public URL will change.

Creating a WhatsApp webhook

Twilio uses the concept of webhooks to enable your application to perform custom actions as a result of external events such as receiving a message from a user on WhatsApp. A webhook is nothing more than an HTTP endpoint that Twilio invokes with information about the event. The response returned to Twilio provides instructions on how to handle the event.

The webhook for an incoming WhatsApp message will include information such as the phone number of the user and the text of the message. In the response, the application can provide a response to send back to the user. The actions that you want Twilio to take in response to an incoming event have to be given in a custom language defined by Twilio that is based on XML and is called TwiML.

Adding a new endpoint

Open the settings.py file from the images directory once again. Find the INSTALLED_APPS variable. This is a list of several strings, which are standard modules of the Django framework. At the end of the list, you need to add one more entry to register the whatsapp application that you created earlier.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'whatsapp.apps.WhatsappConfig',   # ← new item
]

Open the views.py from the whatsapp subdirectory. This is where you are going to create the endpoint that will handle the incoming WhatsApp messages. Replace the contents of this file with the following:

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def message(request):
    return HttpResponse('Hello!')

The message() function is the endpoint function that will run when Twilio notifies the application of an incoming WhatsApp message. For now this function returns a simple response with the text “Hello!”.

To make this endpoint accessible through the web application, a URL needs to be assigned to it. Open the urls.py file from the images directory and add a new entry to the urlpatterns list as shown below:

from django.contrib import admin
from django.urls import path
from whatsapp import views    # ← new import

urlpatterns = [
    path('admin/', admin.site.urls),
    path('message', views.message),    # ← new item
]

The path(‘message’, views.message) line tells Django that the message() function from views.py is mapped to a /message URL on the web application.

To confirm that everything is working, go back to your web browser, and append /message to the ngrok URL you tested earlier. You should see the “Hello!” message that the endpoint returns on the page.

Receiving WhatsApp messages with images

The next step is to update the logic inside the message() endpoint to extract the information about the incoming message. Replace the contents of the views.py file in the whatsapp subdirectory with the following:


from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def message(request):
    user = request.POST.get('From')
    message = request.POST.get('Body')
    media_url = request.POST.get('MediaUrl0')
    print(f'{user} sent {message}')

    return HttpResponse('Hello!')

Twilio will invoke the webhook as a POST request, and it will pass information about the message as form variables, which can be retrieved in Django from the request.POST dictionary. The details about the incoming message are provided as form variables. In particular, the phone number of the user, the text of the message, and an image URL (when an image is included in the message) are useful for this project. These can be obtained from the From, Body and MediaUrl0 fields respectively.

This message function above extracts these three values and then prints the phone number and the message to the console.

Saving the image

The next step is to download the image URL that is stored in the media_url variable, and save it locally. Before you do that, create a dedicated uploads directory where all these images will be stored. Run the following command in the root directory of the project:

mkdir uploads

Inside the uploads directory, the application will create a dedicated sub-directory for each user, which will be identified by their WhatsApp phone number. The images uploaded by a given user will be stored all together in this sub-directory.

Below you can see the updated views.py:


import os    # ← new import
import requests    # ← new import
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def message(request):
    user = request.POST.get('From')
    message = request.POST.get('Body')
    media_url = request.POST.get('MediaUrl0')
    print(f'{user} sent {message}')

    if media_url:
        r = requests.get(media_url)
        content_type = r.headers['Content-Type']
        username = user.split(':')[1]  # remove the whatsapp: prefix from the number
        if content_type == 'image/jpeg':
            filename = f'uploads/{username}/{message}.jpg'
        elif content_type == 'image/png':
            filename = f'uploads/{username}/{message}.png'
        elif content_type == 'image/gif':
            filename = f'uploads/{username}/{message}.gif'
        else:
            filename = None
        if filename:
            if not os.path.exists(f'uploads/{username}'):
                os.mkdir(f'uploads/{username}')
            with open(filename, 'wb') as f:
                f.write(r.content)

    return HttpResponse('Hello!')

The media_url variable is going to be set only if the user sent a message that included a file, so all the logic that handles and saves images is skipped if media_url is set to None.

The file can be downloaded using the requests package, just by sending a GET request to the image URL. Unfortunately the URL does not include a file extension, so the only way to determine if the file that was submitted by the user is an image is to look at the Content-Type header of the response. The code above only accepts JPEG, PNG and GIF images.

When the content type is recognized, the filename variable is set with the location where the file is going to be saved, which is a sub-directory named with the user’s phone number inside the uploads folder created above. For the name of the image file we’ll use the message text, and the extension is determined by the content type.

Before saving the file the code checks if the sub-directory with the user’s phone number exists, and if it doesn’t it creates it. Then the content of the image, which can be obtained from the response object returned by requests, is written to the file.

Sending a response

To complete the webhook you will now send a response back to the user, also through WhatsApp, to let them know that their image has been received (or an error message if they sent an invalid file).

update the response of the endpoint to return TwiML. The twilio package that is installed in the virtual environment provides helper classes that simplify the generation of these responses.

Update the views.py file in the whatsapp subdirectory one last time with the following code:


import os
import requests
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from twilio.twiml.messaging_response import MessagingResponse    # ← new import


@csrf_exempt
def message(request):
    user = request.POST.get('From')
    message = request.POST.get('Body')
    media_url = request.POST.get('MediaUrl0')
    print(f'{user} sent {message}')

    response = MessagingResponse()
    if media_url:
        r = requests.get(media_url)
        content_type = r.headers['Content-Type']
        username = user.split(':')[1]  # remove the whatsapp: prefix from the number
        if content_type == 'image/jpeg':
            filename = f'uploads/{username}/{message}.jpg'
        elif content_type == 'image/png':
            filename = f'uploads/{username}/{message}.png'
        elif content_type == 'image/gif':
            filename = f'uploads/{username}/{message}.gif'
        else:
            filename = None
        if filename:
            if not os.path.exists(f'uploads/{username}'):
                os.mkdir(f'uploads/{username}')
            with open(filename, 'wb') as f:
                f.write(r.content)
            response.message('Thank you! Your image was received.')
        else:
            response.message('The file that you submitted is not a supported image type.')
    else:
        response.message('Please send an image!')
    return HttpResponse(str(response))

This updated implementation of the endpoint uses the MessagingResponse class from the twilio package to build the response that goes back to the user. The message() method of this class instructs Twilio to respond to the user with the message given as an argument. Depending on what happens with the media_url variable the response to the user is set to provide feedback.

Configuring the WhatsApp webhook

To connect the webhook with WhatsApp you need to configure its URL in the Twilio console. Locate the WhatsApp sandbox settings page and edit the “When a message comes in” field with the URL of your webhook. This is going to be the temporary ngrok URL with /message appended at the end. You can see an example below:

WhatsApp webhook configuration

Make sure the dropdown to the right of the URL field is set to “HTTP Post”, and don’t forget to click the “Save” button at the bottom of the page to record these changes.

Testing your WhatsApp messaging service

And now the moment of truth! Make sure the Django server and ngrok are still running. From your smartphone, send a message to the sandbox number. After a short moment you should see a response.

Project demo

If you look in the terminal that is running the Django server you should see that the message and your phone number were received and printed:

WhatsApp terminal

Next steps

I hope you found this tutorial useful. Now that you learned how to receive messages with images on WhatsApp, you may want to take on a slightly more challenging project. If that’s the case, head over to the build a WhatsApp chatbot tutorial.

I’d love to see what you build with WhatsApp and Twilio!

Miguel Grinberg is a Python Developer for Technical Content at Twilio. Reach out to him at mgrinberg [at] twilio [dot] com if you have a cool Python project you’d like to share on this blog!