Ahoy! We now recommend you build your appointment reminders with Twilio's built in Message Scheduling functionality. Head on over to the Message Scheduling documentation to learn more about scheduling messages!
This web application shows how you can use Twilio to send your customers a text message reminding them of upcoming appointments.
We use Flask to build out the web application that supports our user interface, and Celery to send the reminder text messages to our customers at the right time.
In this tutorial, we'll point out the key bits of code that make this application work. Check out the project README on GitHub to see how to run the code yourself.
Check out how Yelp uses SMS to confirm restaurant reservations for diners.
Let's get started! Click the button below to get started.
Before we can use the Twilio API to send reminder text messages, we need to configure our account credentials. These can be found on your Twilio Console. You'll also need an SMS-enabled phone number - you can find or purchase a new one to use here.
We put these environment variables in a .env
file and use autoenv to apply them every time we work on the project. More information on how to configure this application can be found in the project README.
.env.example
_12# Environment variables for appointment-reminders-flask_12_12# App settings_12export DATABASE_URI=_12export SECRET_KEY=asupersecr3tkeyshouldgo_12export CELERY_BROKER_URL=redis://localhost:6379_12export CELERY_RESULT_BACKEND=redis://localhost:6379_12_12# Twilio settings_12export TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXX_12export TWILIO_AUTH_TOKEN=YYYYYYYYYYYYYYYYYY_12export TWILIO_NUMBER=+###########
Now that the configuration is taken care of. We'll move on to the application structure.
The Application
object is the heart of any Flask app. Our's initializes the app, sets the URLs, and pulls in all our environment variables.
The celery
method is boilerplate to configure Celery using settings and context from our Flask application. Our app uses Redis as a broker for Celery. But you can also use any of the other available Celery brokers.
To get Celery to run locally on your machine, follow the instructions in the README.
application.py
_69import flask_69from flask_migrate import Migrate_69_69from flask_sqlalchemy import SQLAlchemy_69_69from celery import Celery_69_69from config import config_classes_69from views.appointment import (_69 AppointmentFormResource,_69 AppointmentResourceCreate,_69 AppointmentResourceDelete,_69 AppointmentResourceIndex,_69)_69_69_69class Route(object):_69 def __init__(self, url, route_name, resource):_69 self.url = url_69 self.route_name = route_name_69 self.resource = resource_69_69_69handlers = [_69 Route('/', 'appointment.index', AppointmentResourceIndex),_69 Route('/appointment', 'appointment.create', AppointmentResourceCreate),_69 Route(_69 '/appointment/<int:id>/delete', 'appointment.delete', AppointmentResourceDelete_69 ),_69 Route('/appointment/new', 'appointment.new', AppointmentFormResource),_69]_69_69_69class Application(object):_69 def __init__(self, routes, environment):_69 self.flask_app = flask.Flask(__name__)_69 self.routes = routes_69 self._configure_app(environment)_69 self._set_routes()_69_69 def celery(self):_69 celery = Celery(_69 self.flask_app.import_name, broker=self.flask_app.config['CELERY_BROKER_URL']_69 )_69 celery.conf.update(self.flask_app.config)_69_69 TaskBase = celery.Task_69_69 class ContextTask(TaskBase):_69 abstract = True_69_69 def __call__(self, *args, **kwargs):_69 with self.flask_app.app_context():_69 return TaskBase.__call__(self, *args, **kwargs)_69_69 celery.Task = ContextTask_69_69 return celery_69_69 def _set_routes(self):_69 for route in self.routes:_69 app_view = route.resource.as_view(route.route_name)_69 self.flask_app.add_url_rule(route.url, view_func=app_view)_69_69 def _configure_app(self, env):_69 self.flask_app.config.from_object(config_classes[env])_69 self.db = SQLAlchemy(self.flask_app)_69 self.migrate = Migrate()_69 self.migrate.init_app(self.flask_app, self.db)
With our Application
ready, let's create an Appointment model
.
Our Appointment model is pretty simple. The name
and phone_number
fields tell us who to send the reminder to. The time
, timezone
, and delta
fields tell us when to send the reminder.
We use SQLAlchemy to power our model and give us a nice ORM interface to use it with.
We added an extra method, get_notification_time
, to help us determine the right time to send our reminders. The handy arrow library makes this kind of time arithmatic easy.
models/appointment.py
_22from database import db_22_22import arrow_22_22_22class Appointment(db.Model):_22 __tablename__ = 'appointments'_22_22 id = db.Column(db.Integer, primary_key=True)_22 name = db.Column(db.String(50), nullable=False)_22 phone_number = db.Column(db.String(50), nullable=False)_22 delta = db.Column(db.Integer, nullable=False)_22 time = db.Column(db.DateTime, nullable=False)_22 timezone = db.Column(db.String(50), nullable=False)_22_22 def __repr__(self):_22 return '<Appointment %r>' % self.name_22_22 def get_notification_time(self):_22 appointment_time = arrow.get(self.time)_22 reminder_time = appointment_time.shift(minutes=-self.delta)_22 return reminder_time
Next we will use this model to create a new Appointment
and schedule a reminder.
This view handles creating new appointments and scheduling new reminders. It accepts POST
data sent to the /appointment
URL.
We use WTForms to validate the form data using a class called NewAppointmentForm
that we defined in forms/new_appointment.py
.
After that we use arrow to convert the time zone of the appointment's time to UTC time.
We then save our new Appointment
object and schedule the reminder using a Celery task we defined called send_sms_reminder
.
views/appointment.py
_57import arrow_57_57from flask.views import MethodView_57from flask import render_template, request, redirect, url_for_57_57from database import db_57from models.appointment import Appointment_57from forms.new_appointment import NewAppointmentForm_57_57_57class AppointmentResourceDelete(MethodView):_57 def post(self, id):_57 appt = db.session.query(Appointment).filter_by(id=id).one()_57 db.session.delete(appt)_57 db.session.commit()_57_57 return redirect(url_for('appointment.index'), code=303)_57_57_57class AppointmentResourceCreate(MethodView):_57 def post(self):_57 form = NewAppointmentForm(request.form)_57_57 if form.validate():_57 from tasks import send_sms_reminder_57_57 appt = Appointment(_57 name=form.data['name'],_57 phone_number=form.data['phone_number'],_57 delta=form.data['delta'],_57 time=form.data['time'],_57 timezone=form.data['timezone'],_57 )_57_57 appt.time = arrow.get(appt.time, appt.timezone).to('utc').naive_57_57 db.session.add(appt)_57 db.session.commit()_57 send_sms_reminder.apply_async(_57 args=[appt.id], eta=appt.get_notification_time()_57 )_57_57 return redirect(url_for('appointment.index'), code=303)_57 else:_57 return render_template('appointments/new.html', form=form), 400_57_57_57class AppointmentResourceIndex(MethodView):_57 def get(self):_57 all_appointments = db.session.query(Appointment).all()_57 return render_template('appointments/index.html', appointments=all_appointments)_57_57_57class AppointmentFormResource(MethodView):_57 def get(self):_57 form = NewAppointmentForm()_57 return render_template('appointments/new.html', form=form)
We'll look at that task next.
Our tasks.py
module contains the definition for our send_sms_reminder
task. At the top of this module we use the twilio-python library to create a new instance of Client
.
We'll use this client
object to send a text message using the Twilio API in our send_sms_reminder
function.
tasks.py
_40import arrow_40_40from celery import Celery_40from sqlalchemy.orm.exc import NoResultFound_40from twilio.rest import Client_40_40from reminders import db, app_40from models.appointment import Appointment_40_40twilio_account_sid = app.config['TWILIO_ACCOUNT_SID']_40twilio_auth_token = app.config['TWILIO_AUTH_TOKEN']_40twilio_number = app.config['TWILIO_NUMBER']_40client = Client(twilio_account_sid, twilio_auth_token)_40_40celery = Celery(app.import_name)_40celery.conf.update(app.config)_40_40_40class ContextTask(celery.Task):_40 def __call__(self, *args, **kwargs):_40 with app.app_context():_40 return self.run(*args, **kwargs)_40_40_40celery.Task = ContextTask_40_40_40@celery.task()_40def send_sms_reminder(appointment_id):_40 try:_40 appointment = db.session.query(Appointment).filter_by(id=appointment_id).one()_40 except NoResultFound:_40 return_40_40 time = arrow.get(appointment.time).to(appointment.timezone)_40 body = "Hello {0}. You have an appointment at {1}!".format(_40 appointment.name, time.format('h:mm a')_40 )_40 to = appointment.phone_number_40 client.messages.create(to, from_=twilio_number, body=body)
Let's look at send_sms_reminder
now.
This is the send_sms_reminder
function we called in our appointment.create
view. Our function starts with an appointment_id
parameter, which we use to retrieve an Appointment
object from the database - a Celery best practice.
To compose the body of our text message, we use arrow again to convert the UTC time stored in our appointment to the local time zone of our customer.
After that, sending the message itself is a simple call to client.messages.create()
. We use our customer's phone number as the to
argument and our Twilio number as the from_
argument.
tasks.py
_40import arrow_40_40from celery import Celery_40from sqlalchemy.orm.exc import NoResultFound_40from twilio.rest import Client_40_40from reminders import db, app_40from models.appointment import Appointment_40_40twilio_account_sid = app.config['TWILIO_ACCOUNT_SID']_40twilio_auth_token = app.config['TWILIO_AUTH_TOKEN']_40twilio_number = app.config['TWILIO_NUMBER']_40client = Client(twilio_account_sid, twilio_auth_token)_40_40celery = Celery(app.import_name)_40celery.conf.update(app.config)_40_40_40class ContextTask(celery.Task):_40 def __call__(self, *args, **kwargs):_40 with app.app_context():_40 return self.run(*args, **kwargs)_40_40_40celery.Task = ContextTask_40_40_40@celery.task()_40def send_sms_reminder(appointment_id):_40 try:_40 appointment = db.session.query(Appointment).filter_by(id=appointment_id).one()_40 except NoResultFound:_40 return_40_40 time = arrow.get(appointment.time).to(appointment.timezone)_40 body = "Hello {0}. You have an appointment at {1}!".format(_40 appointment.name, time.format('h:mm a')_40 )_40 to = appointment.phone_number_40 client.messages.create(to, from_=twilio_number, body=body)
That's it! Our Flask application is all set to send out reminders for upcoming appointments.
We hope you found this sample application useful.
If you're a Python developer working with Twilio and Flask, you might enjoy these other tutorials:
Put a button on your web page that connects visitors to live support or sales people via telephone.
Improve the security of your Flask app's login functionality by adding two-factor authentication via text message.
Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!