๐ Introduction
You know that moment when you try to log into a site and it says:
"We've sent a code to your phone. Please enter it below."
That little 4โ6 digit number is a One-Time Password (OTP) โ your app's way of saying:
"Hey, prove you're really you."
In this guide, we'll make your Django app send OTPs over SMS using Twilio.
We'll also see how to reuse the same setup for sending other types of messages, like deep links.
By the end, you'll have:
โ
A Twilio client class you can reuse anywhere
โ
A background task that sends OTPs without slowing down your app
โ
Proper error handling so your app doesn't freak out when a number is unverified
Step 0: Twilio Setup โ What You Need
Before writing any code, you'll need three pieces of information from your Twilio account:
TWILIO_ACCOUNT_SID
- Your Twilio account's unique identifier.
- Found in the Twilio Console under the Account Info section.
TWILIO_AUTH_TOKEN
- A secret key that authorizes API requests.
- Also in the Twilio Console, right next to your Account SID.
TWILIO_PHONE_NUMBER
- An SMS-capable phone number you've purchased or verified in Twilio.
๐ก Tip: Store these values in your .env file and load them into settings.py with django-environ or a similar library, so you don't expose secrets in your code.
๐ฆ Step 1: Install Twilio SDK
pip install twilio==9.4.*
Or in requirements.txt:
twilio==9.4.*
โ๏ธ Step 2: Configure Twilio in Django Settings
TWILIO_ACCOUNT_SID = "your_account_sid_here"
TWILIO_AUTH_TOKEN = "your_auth_token_here"
TWILIO_PHONE_NUMBER = "+1234567890"
๐ค Step 3: Create a Twilio Client Helper Class
This is our "middleman" โ it knows how to talk to Twilio and can send both OTPs and custom messages.
from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client
from django.conf import settings
class TwilioClient:
"""Handles Twilio SMS sending for OTPs and other messages."""
def __init__(self):
self.account_sid = settings.TWILIO_ACCOUNT_SID
self.auth_token = settings.TWILIO_AUTH_TOKEN
self.twilio_phone_number = settings.TWILIO_PHONE_NUMBER
self.client = Client(self.account_sid, self.auth_token)
def _send_message(self, phone_number, body):
try:
self.client.messages.create(
to=phone_number,
from_=self.twilio_phone_number,
body=body.strip()
)
return {"message": "Message sent successfully.", "status": 201}
except TwilioRestException as e:
if e.code == 21606:
return {"message": "Phone number is unverified.", "status": 400}
return {"message": str(e.msg), "status": 400}
def send_otp(self, otp, phone_number):
body = f"OTP Verification: Please use the following One-Time Password for verification: {otp}"
return self._send_message(phone_number, body)
def send_message(self, message, phone_number):
body = f"Please find the message here: {message}"
return self._send_message(phone_number, body)
โณ Step 4: Generate and Send OTP with Celery
Why Celery?
Because sending SMS can be slow, and we don't want users waiting while Twilio does its thing.
Celery runs the task in the background, so your app stays snappy.
from celery import shared_task
from myapp.services.twilio_client import TwilioClient
import logging
logger = logging.getLogger(__name__)
@shared_task
def send_sms_otp(phone_number, otp):
try:
twilio_client = TwilioClient()
twilio_client.send_otp(otp, phone_number)
except Exception as e:
logger.exception(f"Unable to send OTP SMS to {phone_number} due to {e}")
raise e
OTP Generator
import random
def get_otp():
digits = list(range(0, 10))
random.shuffle(digits)
while True:
if digits[0] == 0:
random.shuffle(digits)
else:
break
return int("".join(map(str, digits[:4])))
How to use it
from myapp.tasks import send_sms_otp
from myapp.utils import get_otp
from django.utils import timezone
from myapp.constants import constance_config
otp = get_otp()
phone_number = "+1234567890"
# Send OTP asynchronously
send_sms_otp.delay(phone_number, otp)
# Optional: store OTP in DB for verification later
# user.otp = otp
# user.otp_expires_at = timezone.now() + timezone.timedelta(
# seconds=constance_config.OTP_EXPIRATION_TIME_SECONDS
# )
# user.save(update_fields=["otp", "otp_expires_at"])
๐งช Step 5: Testing the Flow
- Trigger the Celery task from your Django view:
otp = get_otp()
phone_number = "+1234567890"
send_sms_otp.delay(phone_number, otp)
- Check your Twilio dashboard for message logs.
- Test with both verified and unverified phone numbers to see error handling in action.
๐ฉ Wrapping Up
With Twilio + Django + Celery working together, you've given your app a voice (wellโฆ text).
Now it can ping users instantly, whether it's for OTPs, appointment reminders, or even "your coffee order is ready" alerts.
Remember:
- Keep your Twilio credentials safe.
- Test with both verified and unverified numbers.
- Handle errors gracefully โ nothing kills trust faster than a broken OTP flow.
If this guide helped you, give it a โญ on GitHub or share it with your dev friends.
And if your phone just buzzed, don't panic โ it's probably your app saying hello. ๐ฑโจ
๐ก TL;DR
Goal: Send OTPs from Django via Twilio
Stack: Django + Twilio SDK + Celery
Steps:
1๏ธโฃ Get Twilio credentials (SID, Token, Number)
2๏ธโฃ Install twilio Python package
3๏ธโฃ Create TwilioClient helper class
4๏ธโฃ Generate OTP with get_otp()
5๏ธโฃ Send via Celery background task
Why Celery? No waiting for SMS API โ keeps your app fast
Bonus: Same setup works for deep links, reminders, and custom messages



Top comments (1)
How do I test OTP expiration and retry scenarios?