How to Send OTP in Django (Python) — DRF 2026 Guide
Send and verify SMS OTPs from Django and Django REST Framework using the StartMessaging API. Includes a service module, DRF views, serializers, and rate limiting.
Django and DRF are still the fastest way to ship a Python API in India. This guide adds phone OTP login on top of DRF using the StartMessaging OTP API — no extra packages, no DLT paperwork.
Prerequisites
- Python 3.10+, Django 4.2 / 5.x, djangorestframework.
- A free StartMessaging account and an API key.
# .env
STARTMESSAGING_API_KEY=sm_live_xxxxxxxxxxxxxxxxxxxx
OTP Service Module
# accounts/services/otp.py
import os
import uuid
import requests
BASE_URL = "https://api.startmessaging.com"
HEADERS = {
"Content-Type": "application/json",
"X-API-Key": os.environ["STARTMESSAGING_API_KEY"],
}
class OtpError(Exception):
pass
def send_otp(phone_number: str) -> dict:
res = requests.post(
f"{BASE_URL}/otp/send",
json={"phoneNumber": phone_number, "idempotencyKey": str(uuid.uuid4())},
headers=HEADERS,
timeout=10,
)
if res.status_code >= 400:
raise OtpError(res.json().get("message", "Failed to send OTP"))
return res.json()["data"]
def verify_otp(request_id: str, code: str) -> bool:
res = requests.post(
f"{BASE_URL}/otp/verify",
json={"requestId": request_id, "otpCode": code},
headers=HEADERS,
timeout=10,
)
if res.status_code >= 400:
return False
return res.json()["data"]["verified"]
Serializers
# accounts/serializers.py
from rest_framework import serializers
class SendOtpSerializer(serializers.Serializer):
phone_number = serializers.RegexField(r"^\\+91\\d{10}$")
class VerifyOtpSerializer(serializers.Serializer):
code = serializers.RegexField(r"^\\d{4,6}$")
DRF Views
# accounts/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import SendOtpSerializer, VerifyOtpSerializer
from .services.otp import send_otp, verify_otp, OtpError
class SendOtpView(APIView):
throttle_scope = "otp_send"
def post(self, request):
s = SendOtpSerializer(data=request.data)
s.is_valid(raise_exception=True)
try:
data = send_otp(s.validated_data["phone_number"])
except OtpError as e:
return Response({"error": str(e)}, status=502)
request.session["otp_request_id"] = data["requestId"]
return Response({"expires_at": data["expiresAt"]})
class VerifyOtpView(APIView):
def post(self, request):
s = VerifyOtpSerializer(data=request.data)
s.is_valid(raise_exception=True)
request_id = request.session.pop("otp_request_id", None)
if not request_id:
return Response({"error": "session expired"}, status=400)
if not verify_otp(request_id, s.validated_data["code"]):
return Response({"verified": False}, status=status.HTTP_401_UNAUTHORIZED)
return Response({"verified": True})
URL Routing
# accounts/urls.py
from django.urls import path
from .views import SendOtpView, VerifyOtpView
urlpatterns = [
path("auth/send-otp/", SendOtpView.as_view()),
path("auth/verify-otp/", VerifyOtpView.as_view()),
]
Rate Limiting
Add a DRF throttle scope so a single phone cannot trigger 100 sends per hour:
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.ScopedRateThrottle"],
"DEFAULT_THROTTLE_RATES": {"otp_send": "5/hour"},
}
For the full picture see our OTP rate limiting guide.
Best Practices
- Validate phone numbers with
phonenumbersbefore calling the API. - Use idempotency keys on every send.
- Never log OTP codes — not even in DEBUG.
- Wrap requests in
try/except requests.Timeout.
FAQ
Compare with the Flask version or jump to pricing.
StartMessaging Team
StartMessaging Team