Tutorials

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.

StartMessaging Team Updated

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

# .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 phonenumbers before 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.

S

StartMessaging Team

StartMessaging Team

Related posts