Twilio is all about powering communication and doing it conveniently and fast in any language. But if you have a Python application written with the asyncio package, it may not be completely clear how to translate the examples from the documentation into non-blocking code that works well with an asynchronous loop.
In this tutorial you’ll learn how to properly deliver SMS notifications from a FastAPI application. The techniques shown here are also applicable to other asyncio frameworks.
- Python 3.6 or newer. If your operating system does not provide a Python interpreter, you can go to python.org to download an installer.
- A Twilio account. If you are new to Twilio click here to create a free account now and receive $10 credit when you upgrade to a paid account. You can review the features and limitations of a free Twilio account.
- A smartphone with active service, to test the project.
Buy a Twilio phone number
If you haven't done so already, your first task is to purchase a Twilio phone number to send SMS from.
Log in to the Twilio Console, select Phone Numbers, and then click on the red plus sign to buy a Twilio number. Note that if you have a free account, you will be using your trial credit for this purchase.
On the “Buy a Number” page, select your country and check SMS in the “Capabilities” field. If you’d like to request a number that is local to your region, you can enter your area code in the “Number” field.
Click the “Search” button to see what numbers are available, and then click “Buy” for the number that you like from the results. After you confirm your purchase, click the “Close” button.
Project setup and configuration
In this section you are going to set up a brand new FastAPI 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 fastapi-sms cd fastapi-sms
Creating a virtual environment
Following Python best practices, you are now going to create a virtual environment, where you are going 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:
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
With the virtual environment activated, you are ready to install the Python dependencies required for this project:
pip install fastapi python-dotenv aiofiles python-multipart uvicorn twilio
The six Python packages that this project uses are:
- The FastAPI framework, to create the web application,
- python-dotenv, to import the application configuration from a .env file,
- aiofiles, to give FastAPI the ability to serve static files,
- python-multipart, to give FastAPI the ability to process form data,
- uvicorn, to serve the FastAPI application,
- and the Twilio Python Helper library, to work with the Twilio APIs.
Configure Twilio credentials and phone number
To be able to send an SMS with Twilio, the FastAPI application will need to have access to your Twilio account credentials to authenticate. Also, to send an SMS, you will need to provide a number for the sender, the Twilio number you purchased earlier.
The most secure way to define these configuration values is to set environment variables for them, and the most convenient way to manage your environment variables in a FastAPI application is to use a .env file.
Open a new file named .env (note the leading dot) in your text editor and enter the following contents in it:
TWILIO_ACCOUNT_SID=xxxxxxxxx TWILIO_AUTH_TOKEN=xxxxxxxxx TWILIO_PHONE_NUMBER=xxxxxxxxx
You will need to replace all the
xxxxxxxxx with the correct values that apply to you. The first two variables are your Twilio “Account SID” and your “Auth Token”. You can find them in the dashboard of the Twilio Console:
TWILIO_NUMBER variable is the phone number that you purchased above. When you enter this phone number in the .env file, use the E.164 format, which includes a plus sign and the country code. For example, a number from the United States would be given as
aaa is the area code, and
bbb-cccc is the local number.
To incorporate these three variables into the FastAPI application, create a file named config.py with the following contents:
from pydantic import BaseSettings class Settings(BaseSettings): twilio_account_sid: str twilio_auth_token: str twilio_phone_number: str class Config: env_file = '.env'
FastAPI relies on the
BaseSettings class from
pydantic to manage its configuration. Subclasses of
BaseSettings automatically import variables defined as attributes from environment variables, or directly from the .env file with its dotenv integration.
You will learn how to work with the
Settings class in the next section.
Send an SMS with FastAPI
We are now ready to start coding the FastAPI application. We’ll do it in a few steps.
Base FastAPI application
Below you can see the first iteration of our FastAPI application. This version just returns the main page, which presents a web form where the user can enter the phone number that will receive the SMS.
Open a new file named app.py in your text editor or IDE and enter this code in it:
import asyncio from fastapi import FastAPI, Form, status from fastapi.responses import FileResponse, RedirectResponse from twilio.rest import Client import config app = FastAPI() settings = config.Settings() @app.get('/') async def index(): return FileResponse('index.html')
@app.get(‘/’) decorator defines an endpoint that is mapped to the root URL of the application. The implementation of this endpoint returns a response that is loaded from a static file named index.html.
For this endpoint to work, we now need to create the HTML file. Open a new file named index.html in your editor or IDE and enter the following HTML code in it:
<!doctype html> <html> <head> <title>FastAPI SMS Example</title> </head> <body> <h1>FastAPI SMS Example</h1> <form method="post"> <label for="phone">Your phone number:</label> <input name="phone" id="phone" placeholder="+12345678900"> <input type="submit" value="Send SMS!"> </form> </body> </html>
Running the server
The application is incomplete, but it is functional enough to be started. Make sure you have the app.py, index.html, and .env files created earlier in your project directory, and then start the application using the following command:
uvicorn app:app --reload
Uvicorn is the recommended server to run FastAPI applications. We are starting the server with the
--reload option, which will make uvicorn watch our source files and automatically restart the server when changes are made. You can leave the server running throughout the rest of the tutorial.
To make sure that your application is correct, you can open a web browser and type http://localhost:8000 in the address bar. The browser should load the main page of the application, which looks like this:
Handling form data
If you try to submit the form, FastAPI will return a “Method not allowed” error message. This is because we have not implemented the form submission endpoint yet.
If you look at the
<form> element in index.html, we have defined the form with the
method attribute set to
post and no
action attribute. This means that the form will be submitted with a POST request to the originating URL, which in this case is the root URL of the application.
The endpoint will have the following structure. You can add it at the bottom of app.py, but note the lines that start with
# TODO, which indicate parts of the function that haven’t been built yet.
@app.post('/') async def handle_form(phone: str = Form(...)): # TODO: send the SMS! # TODO: return a response to the client
On this second endpoint we are using the
@app.post(‘/’) decorator to define a handler for
POST requests. The web form that we are using has a single field named
phone, so we have that as an argument into the function. FastAPI will parse the form data and extract the value of this field passed by the client and send it to the function in this argument.
While we have the structure of the function well understood, we have some work to do to send the SMS and to return a response, so we’ll continue working on this endpoint in the following sections.
Sending the SMS
handle_form() function is invoked, we will have a phone number to send an SMS to. We can access the Twilio credentials and the sender’s phone number from the
Settings class we wrote earlier, so at this point we have everything we need to send an SMS with Twilio.
The problem is that the Twilio helper library for Python does not support asynchronous applications. Since this library will be making network requests to Twilio servers, it will block the loop if used directly in the asynchronous function. To avoid this problem, we’ll encapsulate all the Twilio related work in a function that we’ll call
send_sms(), and we’ll execute this function inside an executor to keep the async application running smoothly.
Below you can see an updated version of the
handle_form() function from app.py with the logic to run the
send_sms() function in an executor.
@app.post('/') async def handle_form(phone: str = Form(...)): await asyncio.get_event_loop().run_in_executor( None, send_sms, phone, 'Hello from FastAPI!') # TODO: return a response to the client
run_in_executor() method from the asyncio loop allows you to run a blocking function in a separate thread or process so that the loop does not block. The first argument is the executor that you’d like to use, or
None if you are okay using a default thread executor. The remaining arguments are the function to run and its arguments.
Let’s now have a look at the implementation of the
send_sms() function. Note that this is standard synchronous code, so this function is not defined with the
async keyword. Add this function to app.py:
def send_sms(to_number, body): client = Client(settings.twilio_account_sid, settings.twilio_auth_token) return client.messages.create(from_=settings.twilio_phone_number, to=to_number, body=body)
The function creates an instance of the Twilio client object and initializes it with the Account SID and Auth Token values that come from the
client.messages.create() is used to create and deliver the SMS. This function takes the
body arguments to define the sender, recipient, and body of the SMS respectively. Note that
from_ is used because
from is a reserved keyword in Python.
Sending a response
In the previous section, we left the
handle_form() function incomplete. After the SMS is sent through the executor, the server needs to return a response. The most accepted practice when handling to a form submission is to respond with a redirect, which avoids a number of potential issues including double form submissions and confusing warnings presented to the user by the browser. This practice is known as the Post/Redirect/Get pattern, or PRG.
For this application, we can redirect to a page that indicates that the SMS has been sent successfully, and give the option to the user to send another SMS. Here is the now complete implementation of the
handle_form() function. Make sure you update your version in app.py.
@app.post('/') async def handle_form(phone: str = Form(...)): await asyncio.get_event_loop().run_in_executor( None, send_sms, phone, 'Hello from FastAPI!') return RedirectResponse('/success', status_code=status.HTTP_303_SEE_OTHER)
The response will instruct the browser to immediately redirect to the /success URL. So now we can create a handler for this URL. This handler goes at the end of app.py:
@app.get('/success') async def success(): return FileResponse('success.html')
This is another short handler that renders an HTML page, just like the main one. Create a success.html file and copy the following contents to it:
<!doctype html> <html> <head> <title>FastAPI SMS Example</title> </head> <body> <h1>FastAPI SMS Example</h1> <p>Check your phone!</p> <p><a href="/">Send another one?</a></p> </body> </html>
This page prompts the user to check the message on their phone, and includes a link to the main page, in case the user wants to send another SMS.
Testing the application
And now we have arrived at the moment you’ve been waiting for. The application is complete and we are ready to test it out. Make sure your app.py is updated with all the functions shown above and that you have the config.py, .env, index.html, and success.html files also in your project directory.
If you left
uvicorn running from the start of the tutorial, every time you’ve made an update to the source files, the server should have restarted on its own, so you are ready to go. If you are not running the server, you can start it now with the command:
uvicorn app:app --reload
Open your web browser and navigate to http://localhost:8000. In the form, enter your personal mobile number, using the E.164 format. Click the submit button and in just a moment, you will receive the SMS on your phone!
If you are using a free Twilio account, keep in mind that the number that you use as a recipient must be verified before it can receive SMS. This requirement does not apply if you are using a paid account.
Congratulations on learning how to send SMS with FastAPI! As mentioned in the introduction, the techniques you learned in this article can be applied to other frameworks based on asyncio such as Quart, Sanic, and Tornado. For an in-depth discussion on working with Twilio in your asynchronous applications, check out Using the Twilio Python Helper Library in your Async Applications on this blog.
I’d love to see what you build with Twilio and asyncio!