Developer 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.

19 April 20269 min read

StartMessaging Team

Engineering

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

  1. Validate phone numbers with phonenumbers before calling the API.
  2. Use idempotency keys on every send.
  3. Never log OTP codes — not even in DEBUG.
  4. Wrap requests in try/except requests.Timeout.

FAQ

Compare with the Flask version or jump to pricing.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.