Developer Tutorials

How to Send OTP in Flask (Python) — 2026 Tutorial

Send and verify SMS OTPs from a Flask application using the StartMessaging API. Includes app factory, blueprint routes, sessions, and error handling.

20 April 20268 min read

StartMessaging Team

Engineering

Flask is the go-to micro-framework for small Python services. This guide shows how to drop a phone-OTP login on top of Flask using the StartMessaging OTP API in fewer than 80 lines of code.

Prerequisites

pip install Flask requests python-dotenv Flask-Limiter

App Factory Setup

# app/__init__.py
import os
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(get_remote_address, default_limits=["60 per minute"])

def create_app():
    app = Flask(__name__)
    app.secret_key = os.environ["FLASK_SECRET"]
    limiter.init_app(app)

    from .auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix="/auth")
    return app

OTP Service

# app/services/otp.py
import os, uuid, requests

BASE = "https://api.startmessaging.com"
HEAD = {
    "Content-Type": "application/json",
    "X-API-Key": os.environ["STARTMESSAGING_API_KEY"],
}

def send_otp(phone):
    res = requests.post(
        f"{BASE}/otp/send",
        json={"phoneNumber": phone, "idempotencyKey": str(uuid.uuid4())},
        headers=HEAD, timeout=10,
    )
    res.raise_for_status()
    return res.json()["data"]

def verify_otp(request_id, code):
    res = requests.post(
        f"{BASE}/otp/verify",
        json={"requestId": request_id, "otpCode": code},
        headers=HEAD, timeout=10,
    )
    return res.ok and res.json()["data"]["verified"]

Auth Blueprint

# app/auth.py
from flask import Blueprint, request, session, jsonify
from .services.otp import send_otp, verify_otp
from . import limiter

bp = Blueprint("auth", __name__)

@bp.post("/send-otp")
@limiter.limit("5 per hour", key_func=lambda: request.json.get("phone", ""))
def send():
    phone = request.json.get("phone", "")
    try:
        data = send_otp(phone)
    except Exception as e:
        return jsonify(error=str(e)), 502
    session["otp_request_id"] = data["requestId"]
    return jsonify(expires_at=data["expiresAt"])

@bp.post("/verify-otp")
def verify():
    code = request.json.get("code", "")
    rid = session.pop("otp_request_id", None)
    if not rid:
        return jsonify(error="session expired"), 400
    if not verify_otp(rid, code):
        return jsonify(verified=False), 401
    session["user_phone"] = request.json.get("phone")
    return jsonify(verified=True)

Sessions and Cookies

Flask’s built-in cookie session is signed but not encrypted, so only store opaque values like the requestId — never the OTP itself. For higher-security apps, use server-side sessions via Flask-Session backed by Redis.

Best Practices

  1. Use Flask-Limiter on the send endpoint, keyed by phone.
  2. Always set an HTTP timeout on the requests call.
  3. Never log request.json on the verify endpoint.
  4. For background sends, queue them via Celery or RQ so the request returns instantly.

FAQ

See the equivalent Django guide if you prefer DRF, or check our OTP flow design article.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.