top of page

What is an OTP SMS API? How One-Time Password Delivery Works — A Complete Developer Guide

Updated: May 12

Smartphone screen with "OTP," surrounded by checkmarks, padlock, and icons on a blue background. Text reads "The Best OTP SMS API."


What is an OTP SMS API? An OTP SMS API is a programmatic interface that allows applications to automatically generate a cryptographically secure one-time password, send it to a user's mobile number via SMS through a gateway provider, and verify the code the user enters — all without the application storing sensitive passwords on its own servers. The complete flow is: (1) your application triggers an API call when a user requests authentication; (2) the API generates a secure random code; (3) the gateway delivers the code via SMS to the user's handset; (4) the user enters the code; (5) your application calls the verify endpoint to confirm the code is correct, unexpired, and unused. In India, every OTP SMS API must be TRAI DLT Service Implicit (SI) compliant.

An OTP SMS API is the authentication infrastructure that powers every "Enter the 6-digit code sent to your mobile" moment in India's digital economy — from UPI payment confirmations to app signups, hospital patient portals to banking logins. Understanding how an OTP SMS API works is not just useful for building authentication features; it is essential for building them correctly. The difference between a correctly implemented OTP flow and a poorly implemented one is the difference between a secure, frictionless user experience and a system vulnerable to OTP bombing, session fixation, brute-force attacks, and silent delivery failures. This guide covers the complete technical flow, production-ready code examples, India-specific TRAI DLT compliance requirements, security best practices, and the 12 most expensive mistakes developers make when building OTP SMS systems.

What is an OTP SMS API? — Complete Technical Definition

A one-time password (OTP) is a temporary, randomly generated code that is valid for a single authentication session and expires after a defined time window — typically 5–10 minutes for standard authentication, 30–120 seconds for high-security payment flows. Unlike static passwords, an OTP cannot be reused once successfully verified or once expired, dramatically reducing the attack surface for credential theft and replay attacks.

An OTP SMS API has three components:

1. The Generation Engine Produces a cryptographically secure random code of defined length (typically 6 digits). The word "cryptographically secure" is not stylistic — it refers specifically to whether the random number generator uses an entropy source (OS-level randomness: /dev/urandom on Linux, CryptGenRandom on Windows) that cannot be predicted by an attacker. A non-cryptographic RNG like Python's random.randint() can be predicted after observing enough outputs.

2. The Delivery Channel Routes the generated code as an SMS message to the user's mobile number. For Indian applications, this delivery must happen through a TRAI DLT-registered gateway on the Service Implicit (SI) route — a dedicated OTP priority channel that bypasses promotional and standard transactional queues.

3. The Verification Engine Accepts the code entered by the user, compares it to the stored (hashed) original, validates that it has not expired, validates that it has not already been used, and validates that the requesting session matches the originating request. Returns a simple verified/rejected boolean that your application uses to grant or deny access.

OTP vs 2FA vs TOTP — Understanding the Terminology

Developers frequently confuse these three related but distinct concepts:

Term

Definition

What It Is

OTP

One-Time Password

A single code valid for one session — the code itself

2FA

Two-Factor Authentication

A security process requiring two identity proofs

TOTP

Time-Based One-Time Password

OTPs generated by an authenticator app using a shared secret (RFC 6238)

OTP SMS API

Delivery mechanism

The infrastructure that generates and delivers OTPs via SMS

2FA is the process. OTP is one implementation of 2FA. TOTP (Google Authenticator, Authy) is an OTP variant that doesn't use SMS delivery. An OTP SMS API is specifically the SMS delivery pathway for OTP-based 2FA.

When to use SMS OTP vs TOTP:

  • SMS OTP: Universal reach — works on every phone, no app required, no shared secret to manage. Higher friction for users but maximum coverage including feature phones and first-time users.

  • TOTP: More secure (not susceptible to SIM swapping), better UX for frequent users. Requires app installation and initial setup.

For most Indian consumer applications — where maximum reach including rural and semi-urban users matters — SMS OTP is the appropriate primary authentication channel, with TOTP as an optional advanced setting for power users.


How an OTP SMS API Works — The Complete Technical Flow

Understanding the full journey from trigger to verified access is the foundation of building a correct, secure OTP system. Here is the complete technical flow with every component explained:

COMPLETE OTP SMS API FLOW:

USER ACTION (click "Login" / "Pay" / "Verify")
     │
     ▼
YOUR APPLICATION BACKEND
  ├── Validate phone number format (+91XXXXXXXXXX)
  ├── Check rate limit: max 3 OTP requests per number per 10 minutes
  ├── Generate cryptographically secure 6-digit OTP
  ├── Hash OTP with salt → store hash + expiry in session/database
  └── POST to OTP SMS API endpoint
     │
     ▼
OTP SMS API GATEWAY (Techto Networks)
  ├── Authenticate API key
  ├── Validate TRAI DLT template_id (India-specific)
  ├── Classify route: Service Implicit (SI) → OTP Priority Queue
  ├── HLR lookup: identify recipient's operator (Jio/Airtel/Vi/BSNL)
  └── Submit to operator SMSC via SMPP bind
     │
     ▼
TELECOM OPERATOR SMSC
  ├── DLT scrubbing: validate template match and entity registration
  ├── Route to recipient's cell tower
  └── Deliver to handset
     │
     ▼
USER'S PHONE receives SMS: "847291 is your OTP for MyApp. Valid 10 minutes."
     │
     ▼
USER ENTERS OTP in your app
     │
     ▼
YOUR APPLICATION BACKEND
  ├── POST to OTP Verify endpoint: {message_id, phone, otp_entered}
  ├── Gateway verifies: code match + not expired + not already used
  └── Response: {verified: true} or {verified: false, reason: "OTP_EXPIRED"}
     │
     ▼
GRANT ACCESS or REJECT with error message

Step 1: Trigger — The User Action That Starts the OTP Flow

The OTP flow begins when a user takes an action that requires identity verification: registering a new account, logging in from an unrecognised device, initiating a payment above a threshold, changing account settings, or requesting a password reset.

Your application should not automatically trigger an OTP on every page load or minor interaction — over-triggering trains users to ignore OTPs, increases costs, and creates rate-limiting compliance challenges.

When to trigger an OTP SMS API call:

  • First-time phone number registration / verification

  • Login from new device or new location (risk-based authentication)

  • Payment or transfer above a defined value threshold

  • Sensitive account changes (email, phone, password update)

  • Data access requiring explicit consent verification (DPDP Act 2023 compliance for health data)

What to validate before triggering:

python

import re
from datetime import datetime, timedelta

def validate_otp_trigger(phone_number: str, user_ip: str) -> dict:
    """
    Validate all conditions before triggering OTP SMS API call.
    Returns: {valid: bool, reason: str}
    """
    
    # 1. Phone number format validation
    if not re.match(r'^91[6-9]\d{9}$', phone_number):
        return {"valid": False, "reason": "Invalid Indian mobile number format"}
    
    # 2. Rate limit check — max 3 OTPs per number per 10 minutes
    recent_count = otp_requests.count(
        phone=phone_number,
        after=datetime.now() - timedelta(minutes=10)
    )
    if recent_count >= 3:
        return {"valid": False, "reason": "Too many OTP requests. Try again in 10 minutes."}
    
    # 3. IP-level rate limit — max 10 OTP requests per IP per hour
    ip_count = otp_requests.count(
        ip=user_ip,
        after=datetime.now() - timedelta(hours=1)
    )
    if ip_count >= 10:
        return {"valid": False, "reason": "Too many requests from this network."}
    
    # 4. Block previously flagged/suspended numbers
    if is_flagged_number(phone_number):
        return {"valid": False, "reason": "Number temporarily suspended for security reasons."}
    
    return {"valid": True}

Step 2: OTP Generation — Cryptographically Secure vs Insecure

This step has the highest security variance in the entire OTP flow. The difference between a secure and insecure OTP system often comes down to a single function call.

The wrong way (insecure — DO NOT USE):

python

# ❌ INSECURE — Python random module is predictable
import random
otp = str(random.randint(100000, 999999))

# ❌ INSECURE — Math.random() in JavaScript is not CSPRNG
const otp = Math.floor(Math.random() * 900000 + 100000).toString();

# ❌ INSECURE — PHP rand() is not cryptographically secure
$otp = rand(100000, 999999);

These functions use pseudorandom number generators (PRNGs) whose internal state can be determined by an attacker who observes enough outputs. In a high-volume OTP system, this creates a realistic prediction attack surface.

The right way (cryptographically secure CSPRNG):

python

# ✅ SECURE — Python secrets module uses OS entropy (/dev/urandom)
import secrets

def generate_otp(length: int = 6) -> str:
    """Generate a cryptographically secure numeric OTP."""
    # Generates a number in range [10^(length-1), 10^length - 1]
    lower = 10 ** (length - 1)
    upper = 10 ** length - 1
    return str(secrets.randbelow(upper - lower + 1) + lower)

javascript

// ✅ SECURE — Node.js crypto.randomInt() uses OS entropy
const crypto = require('crypto');

function generateOTP(length = 6) {
  const min = Math.pow(10, length - 1);
  const max = Math.pow(10, length) - 1;
  return crypto.randomInt(min, max + 1).toString();
}

php

<?php
// ✅ SECURE — PHP random_int() uses OS-level CSPRNG
function generateOTP(int $length = 6): string {
    $min = (int) pow(10, $length - 1);
    $max = (int) pow(10, $length) - 1;
    return (string) random_int($min, $max);
}

java

// ✅ SECURE — Java SecureRandom uses OS entropy
import java.security.SecureRandom;

public class OTPGenerator {
    private static final SecureRandom secureRandom = new SecureRandom();
    
    public static String generateOTP(int length) {
        int min = (int) Math.pow(10, length - 1);
        int max = (int) Math.pow(10, length) - 1;
        int otp = min + secureRandom.nextInt(max - min + 1);
        return String.valueOf(otp);
    }
}

Step 3: OTP Storage — Never Store Plaintext

Once generated, the OTP must be stored server-side for later verification. The most common mistake at this step is storing the plaintext OTP code in the database. If your database is compromised, every stored OTP is immediately exploitable.

Store the hash, not the code:

python

import hashlib
import secrets
import time

def store_otp_for_verification(
    otp: str, 
    phone: str, 
    expiry_seconds: int = 300
) -> dict:
    """
    Hash and store OTP for later verification.
    NEVER store plaintext OTP.
    """
    # Generate a random salt per OTP — prevents rainbow table attacks
    salt = secrets.token_hex(16)
    
    # Hash OTP + salt with SHA-256
    otp_hash = hashlib.sha256(f"{otp}{salt}".encode()).hexdigest()
    
    otp_record = {
        "phone": phone,
        "otp_hash": otp_hash,    # Store hash, never plaintext
        "salt": salt,
        "expires_at": time.time() + expiry_seconds,
        "verified": False,
        "attempts": 0,
        "created_at": time.time()
    }
    
    # Store in database or Redis (Redis recommended for auto-expiry via TTL)
    redis_client.setex(
        f"otp:{phone}",
        expiry_seconds,
        json.dumps(otp_record)
    )
    
    return {"stored": True, "expires_in_seconds": expiry_seconds}

Alternative: Use the gateway's Verify API Techto Networks' OTP Verify API eliminates the need to store OTPs in your system entirely. You pass the message_id returned from the send call to the verify endpoint — the gateway handles storage, hashing, expiry tracking, and attempt counting server-side.

Step 4: Delivery — The OTP SMS API Call

With a generated, stored OTP, your application makes the API call to Techto Networks' OTP SMS API endpoint:

python

import os
import requests

def send_otp_via_sms(
    phone_number: str,
    otp_code: str,
    app_name: str = "MyApp",
    expiry_minutes: int = 10
) -> dict:
    """
    Send OTP via Techto Networks OTP SMS API.
    
    Args:
        phone_number: E.164 format (e.g., "919876543210")
        otp_code: The generated secure OTP
        app_name: App name for the OTP message template
        expiry_minutes: OTP validity window for message text
    
    Returns:
        dict with message_id, status, estimated_delivery_seconds
    """
    response = requests.post(
        "https://api.techtonetworks.com/v1/otp/send",
        json={
            "to": phone_number,
            "from": os.environ["TECHTO_SENDER_ID"],
            "type": "OTP",
            "template_id": os.environ["TECHTO_OTP_TEMPLATE_ID"],
            "variables": {
                "otp": otp_code,
                "app_name": app_name,
                "expiry_minutes": str(expiry_minutes)
            },
            "route": "OTP_PRIORITY",
            # Voice fallback: auto-call user if SMS fails after 30s
            "voice_fallback": True,
            "fallback_after_seconds": 30,
            "voice_language": "hindi",  # Options: hindi, english, tamil, telugu...
            "otp_config": {
                "max_retries": 3,
                "resend_cooldown_seconds": 60,
                "max_resends": 3
            }
        },
        headers={
            "Authorization": f"Bearer {os.environ['TECHTO_API_KEY']}"
        },
        timeout=10
    )
    response.raise_for_status()
    return response.json()

# Example usage
result = send_otp_via_sms(
    phone_number="919876543210",
    otp_code="847291",
    app_name="MyFinApp",
    expiry_minutes=5  # 5 minutes for payment OTP (RBI recommendation)
)

print(f"Message ID: {result['message_id']}")
print(f"Estimated delivery: {result['estimated_delivery_seconds']}s")
# Store result['message_id'] for later verification

What happens during delivery:

The OTP SMS API gateway receives your request and executes a precise sequence:

  1. API key authentication — verifies your account is active and has sufficient credits

  2. TRAI DLT validation — matches your message content against the registered SI-category OTP template (India-specific — see compliance section below)

  3. Route assignment — OTP messages are classified to the dedicated priority route, separate from all promotional and standard transactional queues

  4. HLR lookup — identifies the recipient's current operator including MNP (Mobile Number Portability) — ensures a number ported from Airtel to Jio routes to Jio's SMSC

  5. SMPP submission — message submitted to the operator's SMSC via direct Tier-1 connection

  6. DLR return — operator confirms delivery; gateway fires webhook to your server

OTP delivery speed by Indian operator (Techto Networks priority route):

Operator

Average Delivery

95th Percentile

99th Percentile

Jio

1.2 seconds

2.1 seconds

2.8 seconds

Airtel

1.4 seconds

2.4 seconds

3.1 seconds

Vodafone Idea

1.7 seconds

2.9 seconds

3.6 seconds

BSNL

2.1 seconds

3.4 seconds

4.2 seconds

Step 5: Delivery Confirmation — Webhook DLR Handling

Your application should know when an OTP is delivered — not just when it is submitted. A message submitted to the gateway is not the same as a message delivered to a handset. Webhook DLRs (Delivery Receipts) provide real-time, operator-confirmed delivery status.

python

# Flask webhook endpoint — receives OTP delivery status
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route('/webhooks/otp-dlr', methods=['POST'])
def handle_otp_dlr():
    dlr = request.json
    
    message_id = dlr['message_id']
    status = dlr['status']        # DELIVERED, FAILED, PENDING, VOICE_INITIATED
    network = dlr.get('network')  # Jio, Airtel, Vodafone Idea, BSNL
    latency_ms = dlr.get('latency_ms')
    reason = dlr.get('reason')    # For FAILED: error category
    
    if status == 'DELIVERED':
        # OTP confirmed on handset — log for audit
        log_otp_delivery(message_id, network, latency_ms)
        
    elif status == 'FAILED':
        # SMS failed — check if voice fallback is handling it
        if reason == 'NETWORK_ERROR':
            # System is retrying on backup route automatically
            pass
        elif reason == 'INVALID_NUMBER':
            # Permanent failure — mark number as invalid
            flag_invalid_number(dlr['to'])
        elif reason == 'DLT_TEMPLATE_MISMATCH':
            # Compliance issue — alert operations team immediately
            notify_compliance_team(message_id, reason)
    
    elif status == 'VOICE_INITIATED':
        # SMS timed out — voice fallback call initiated automatically
        log_voice_fallback_triggered(message_id, dlr['to'])
    
    return jsonify({'received': True}), 200

Step 6: OTP Verification — The Final Confirmation

When the user enters the OTP code, your application must verify it. Two approaches:

Option A: Techto Networks Verify API (Recommended)

No database storage required. The gateway tracks expiry, attempt counts, and code matching server-side:

python

def verify_otp(message_id: str, phone_number: str, user_input: str) -> dict:
    """
    Verify OTP entered by user via Techto Networks Verify API.
    No OTP stored in your database required.
    """
    response = requests.post(
        "https://api.techtonetworks.com/v1/otp/verify",
        json={
            "message_id": message_id,  # From the send call
            "to": phone_number,
            "otp_entered": user_input
        },
        headers={"Authorization": f"Bearer {os.environ['TECHTO_API_KEY']}"},
        timeout=5
    )
    
    result = response.json()
    # result = {"verified": true, "verified_at": "..."} 
    # OR {"verified": false, "reason": "OTP_EXPIRED|OTP_MISMATCH|MAX_ATTEMPTS_EXCEEDED"}
    
    return result

# In your authentication endpoint:
@app.route('/api/verify-otp', methods=['POST'])
def verify_otp_endpoint():
    data = request.json
    
    result = verify_otp(
        message_id=session.get('otp_message_id'),
        phone_number=session.get('otp_phone'),
        user_input=data.get('otp_code')
    )
    
    if result.get('verified'):
        session.pop('otp_message_id', None)
        session.pop('otp_phone', None)
        return jsonify({'success': True, 'message': 'Phone verified'})
    
    reason = result.get('reason', 'UNKNOWN')
    
    if reason == 'OTP_EXPIRED':
        return jsonify({'success': False, 'error': 'OTP has expired. Please request a new one.'}), 400
    elif reason == 'MAX_ATTEMPTS_EXCEEDED':
        return jsonify({'success': False, 'error': 'Too many failed attempts. Request a new OTP.'}), 429
    else:
        return jsonify({'success': False, 'error': 'Incorrect OTP. Please try again.'}), 400

Option B: Local Verification (When Gateway Verify API Not Available)

python

def verify_otp_locally(phone: str, user_input: str) -> dict:
    """Local OTP verification using stored hash."""
    
    record = redis_client.get(f"otp:{phone}")
    if not record:
        return {"verified": False, "reason": "OTP_NOT_FOUND_OR_EXPIRED"}
    
    otp_data = json.loads(record)
    
    # 1. Check expiry
    if time.time() > otp_data['expires_at']:
        redis_client.delete(f"otp:{phone}")
        return {"verified": False, "reason": "OTP_EXPIRED"}
    
    # 2. Check attempts
    if otp_data['attempts'] >= 3:
        redis_client.delete(f"otp:{phone}")
        return {"verified": False, "reason": "MAX_ATTEMPTS_EXCEEDED"}
    
    # 3. Increment attempt count
    otp_data['attempts'] += 1
    redis_client.setex(f"otp:{phone}", 
                       int(otp_data['expires_at'] - time.time()),
                       json.dumps(otp_data))
    
    # 4. Verify hash
    input_hash = hashlib.sha256(
        f"{user_input}{otp_data['salt']}".encode()
    ).hexdigest()
    
    if input_hash != otp_data['otp_hash']:
        return {"verified": False, "reason": "OTP_MISMATCH"}
    
    # 5. Mark as used and delete
    redis_client.delete(f"otp:{phone}")
    return {"verified": True}

TRAI DLT Compliance for OTP SMS API in India — What Every Developer Must Know

India's TRAI DLT mandate transforms the OTP SMS API integration for any application serving Indian users. This section is what no global OTP API guide covers — and what every Indian developer needs to know before the first production OTP send.

Why OTP SMS Is "Service Implicit" — Not "Transactional"

The single most common TRAI DLT classification mistake for OTP messages is registering them as "Transactional (T)" instead of "Service Implicit (SI)". This matters because:

  • Wrong category → slower route: Transactional route (3–8 seconds average) vs OTP SI priority route (1–3 seconds average)

  • Wrong category → potential SMSC rejection: If the operator's DLT classification mismatch detection flags your template

  • Banking OTP specifically: RBI's banking OTP rules apply to the SI-OTP sub-type — using Category T for banking authentication creates compliance risk

Why OTP is SI (Service Implicit), not T (Transactional): Transactional SMS is triggered by a specific customer action (order placed, payment confirmed). Service Implicit SMS covers communications where the customer has an implied service relationship — including authentication flows where the user is identifying themselves to access a service they already use. OTP verification is SI because the user is accessing a service, not completing a discrete transaction.

OTP Template Registration on TRAI DLT Portal — Step-by-Step

TRAI DLT OTP Template Requirements:

1. Category: Service Implicit (SI)
2. Sub-type: OTP
3. Sender ID format: Alphanumeric, 6 characters
   Examples: TN-APPNM, VM-MYAPP, AR-LOGINP
4. Template format (locked — CANNOT deviate at send time):
   - OTP code: {#var#}
   - App/service name: {#var#}
   - Expiry: {#var#}
   - "Do not share": mandatory static text

VALID TEMPLATE EXAMPLES:
"Your OTP for {#var#} is {#var#}. Valid for {#var#} minutes. Do not share."
"{#var#} is your verification code for {#var#}. Valid {#var#} mins. Do not share."
"Use {#var#} to verify your {#var#} account. Expires in {#var#} minutes. Do not share."

INVALID — WILL BE REJECTED:
❌ Contains URL: "Your OTP is {#var#}. Login: app.example.com"
❌ Contains promotional text: "Your OTP is {#var#}. Get 20% off with code SAVE20"
❌ Multiple OTP vars adjacent: "{#var#}{#var#} is your code"
❌ Missing expiry: "Your OTP is {#var#}. Do not share."
❌ Missing "Do not share": "Your OTP for {#var#} is {#var#}. Valid {#var#} mins."

Banking OTP — Additional RBI Restrictions (2024 Circular, enforced 2026):

BANKING OTP TEMPLATE RULES:
❌ No URLs of any kind (bit.ly, app links, www links — all prohibited)
❌ No alphanumeric OTP codes — numeric only
✅ Maximum 5-minute expiry for payment authentication OTPs
✅ Mandatory "Do not share" in exact wording
✅ Bank/institution name must appear in static text or var

COMPLIANT BANKING OTP TEMPLATE:
"{#var#} is your {#var#} OTP. Valid {#var#} minutes. Do not share with anyone."

NON-COMPLIANT (will be blocked at operator):
"{#var#} is your payment OTP. Proceed: pay.mybank.com/verify"

The Critical TRAI Template Match Failure — Silent Blocking Explained

The most dangerous DLT compliance failure mode for OTP SMS API integrations: your API call returns success, your application logs delivery, but the user never receives the OTP.

When this happens: The submit_sm_resp (or HTTP 200 response) confirms the gateway accepted your message. But the operator's SMSC-level DLT scrubbing engine detects that your message content does not match the registered template structure — and silently drops the message.

Common causes:

  • Variable field populated with content longer or differently formatted than the template implies (e.g., an 8-character OTP in a template that shows a 6-character OTP in the sample)

  • Extra whitespace or punctuation added programmatically that wasn't in the registered template

  • Variable order mismatch (OTP in position 2 of template, but your API call passes app name in variable 2)

Techto Networks' protection: Our OTP SMS API validates message content against your registered template before submission to the operator. A DLT_TEMPLATE_MISMATCH error is returned at the API level — preventing silent blocking and preserving credits.

Code to handle DLT errors:

python

try:
    result = send_otp_via_sms(phone_number, otp_code)
    
except requests.HTTPError as e:
    error = e.response.json()
    error_code = error.get('error_code')
    
    if error_code == 'DLT_TEMPLATE_MISMATCH':
        # Content doesn't match registered template
        # Check variable order and content format
        alert_ops_team(f"DLT mismatch for {phone_number}: {error}")
        return error_response("Authentication temporarily unavailable", 503)
    
    elif error_code == 'INVALID_SENDER_ID':
        # Sender ID not registered for SI category
        alert_ops_team(f"Sender ID issue: {error}")
        return error_response("Authentication temporarily unavailable", 503)
    
    elif error_code == 'INSUFFICIENT_CREDITS':
        # Credit balance exhausted
        alert_ops_team("OTP API credits exhausted — top up immediately")
        return error_response("Authentication temporarily unavailable", 503)
    
    else:
        # Log unexpected error but don't expose internals to user
        log_error(error_code, phone_number)
        return error_response("Please try again", 500)

OTP SMS API Security Best Practices

1. OTP Expiry — The Temporal Security Layer

The time window during which an OTP is valid is your first line of defence against offline brute-force and interception attacks. An OTP that is valid indefinitely is no longer a one-time password — it is a static credential.

Recommended expiry windows:

Use Case

Recommended Expiry

Rationale

Login / signup verification

5–10 minutes

Balance user convenience with security

Payment authentication

2–5 minutes

RBI recommendation; high-value action warrants tighter window

Password reset

15 minutes

User may not be at their phone immediately

High-security operations (admin access, account deletion)

60–120 seconds

Tightest window for highest-risk actions

WhatsApp/email OTP (TOTP alternative)

30–60 seconds

Channel latency is lower, so shorter window is safe

2. Rate Limiting — Preventing OTP Bombing

OTP bombing is an attack where a malicious actor triggers hundreds or thousands of OTP sends to a victim's number, causing harassment, exhausting your SMS credits, and overwhelming the victim's inbox to prevent them from finding legitimate OTPs.

It is also a denial-of-service vector: if your application sends an OTP on every login attempt without rate limiting, an attacker can lock a victim's account by exhausting your send limit for that number.


3. Attempt Limiting — Preventing Brute-Force Verification

Even with rate limiting on the send side, a determined attacker can try to guess a 6-digit OTP by attempting 1,000,000 verification calls. At 1,000 requests/second, they could exhaust the key space in 16 minutes without attempt limiting.

Maximum attempts per OTP: 3–5


4. Secure OTP Transmission to the Client

After verification, your server must issue a session token to the client — the OTP should never be the session credential itself.


5. Voice OTP Fallback — Ensuring Delivery When SMS Fails

For applications where authentication is business-critical — fintech checkout, banking login, healthcare portals — a failed SMS OTP is a failed transaction. Voice OTP fallback ensures users can still authenticate when SMS delivery fails due to network congestion, SMSC overload, or users in low-signal areas.

When to configure voice fallback:

  • Financial applications where failed OTP = lost transaction revenue

  • Healthcare platforms where authentication failure may delay patient access

  • Government services where every citizen must be able to authenticate regardless of connectivity

  • Applications serving Tier-3 cities and rural India with inconsistent SMS delivery


6. WebOTP API — Auto-Read OTP on Android

The WebOTP API (W3C specification) allows Android browsers to automatically read incoming OTP SMS messages and fill the input field — eliminating the manual copy-paste step and significantly improving OTP UX on Android.


12 Common OTP SMS API Mistakes Developers Make

These are the errors that cause production incidents, security vulnerabilities, and poor user experience — ordered by frequency and severity:


Mistake 1: Using Non-Cryptographic Random Number Generators

random.randint() in Python, Math.random() in JavaScript, rand() in PHP — these use PRNGs whose internal state can be reverse-engineered by an attacker with enough OTP observations. Always use secrets, crypto.randomInt(), random_int(), or SecureRandom respectively.


Mistake 2: Storing Plaintext OTPs in the Database

If your database is compromised and you store plaintext OTPs, every current OTP is immediately exploitable. Store only the salted SHA-256 hash, or use the gateway's Verify API which eliminates database storage entirely.


Mistake 3: Missing Rate Limiting on the Send Endpoint

An unprotected /api/send-otp endpoint can be exploited for OTP bombing (harassing users with unsolicited SMS), credit exhaustion, and enumeration attacks (confirming which phone numbers are registered). Implement per-number, per-IP, and per-account limits.


Mistake 4: No Attempt Limiting on the Verify Endpoint

Without limiting verification attempts per OTP, a 6-digit OTP (1,000,000 combinations) can be brute-forced in 16 minutes at 1,000 requests/second. Limit to 3–5 attempts and invalidate the OTP on max attempts.


Mistake 5: Excessively Long OTP Expiry Windows

OTPs valid for 30 minutes or indefinitely are effectively static passwords for that window. A user who receives an OTP and steps away from their phone has a 30-minute window during which anyone with access to their phone can authenticate. Use 5–10 minutes for login and 2–5 minutes for payment.


Mistake 6: Sending OTP on Every Page Load (No Trigger Control)

Some implementations send an OTP every time the authentication page is loaded — including when the back button is pressed. This inflates SMS costs, confuses users who receive multiple OTPs, and creates rate-limiting issues. OTP sends must be explicitly user-initiated.


Mistake 7: Returning the OTP in the API Response to the Client

The OTP must never be included in the API response to your client. The client receives only a confirmation that an OTP was sent. The OTP itself travels via the out-of-band SMS channel — that separation is the security model.


Mistake 8: Allowing OTP Reuse After Successful Verification

Once an OTP is successfully verified, it must be immediately invalidated and deleted from storage. An OTP that remains valid after use can be replayed in a session fixation attack if an attacker intercepts the code after verification.


Mistake 9: Not Handling the TRAI DLT Template Mismatch (India-Specific)

Indian developers integrating OTP SMS APIs for the first time often skip TRAI DLT registration or register templates with minor formatting differences from their production messages. The result: 100% message blocking with no error visible in the application layer. Always register your exact production template and validate locally before sending.


Mistake 10: No Voice Fallback for Critical Flows

Payment applications that only use SMS for OTP delivery will experience failed transactions every time SMS delivery is delayed on BSNL or Vi networks, or when a user is in a low-signal area. Voice OTP fallback ensures authentication works even when SMS doesn't.


Mistake 11: Using the Same OTP Template for Banking and Non-Banking Flows

RBI's 2024 circular specifically prohibits URLs in banking OTP messages — but URLs are permitted in non-banking OTP messages (app deep links, etc.). Using one template for both means your banking OTPs will be blocked by operators (if the template contains URLs) or your non-banking OTPs cannot include app links (if you register without URLs). Maintain separate templates for banking and non-banking OTP flows.


Mistake 12: Not Logging OTP Events for Fraud Investigation

When a user reports an unauthorised login via OTP, your security team needs to answer: When was the OTP generated? When was it delivered? When was it verified? From what IP? Without OTP event logging, these questions cannot be answered. Log: send timestamp, delivery confirmation timestamp, verify timestamp, user IP, device fingerprint, and the hash of the OTP (not plaintext).

Frequently Asked Questions — OTP SMS API

Q: What is an OTP SMS API? An OTP SMS API is a programmatic interface that allows applications to automatically generate a cryptographically secure one-time password and deliver it to a user's mobile number via SMS. The complete flow is: application triggers the API with the recipient's phone number → API generates a secure random code → delivers it as SMS through the gateway → user enters the code → application calls the verify endpoint to confirm the code is correct, unexpired, and unused. In India, OTP SMS APIs must route through TRAI DLT-registered gateways under the Service Implicit (SI) category.

Q: How does an OTP SMS API work technically? The technical flow has six steps: (1) Your application generates a cryptographically secure random 6-digit code using the OS-level CSPRNG — never a pseudorandom library like Python's random module. (2) Your application makes an HTTPS POST request to the OTP SMS API endpoint with the phone number, the OTP value as a template variable, and the TRAI DLT Template ID. (3) The gateway validates DLT compliance, classifies the message on the OTP priority route, and submits to the recipient's telecom operator via SMPP. (4) The operator delivers the SMS to the handset and returns a delivery receipt. (5) The user enters the code in your application. (6) Your application calls the verify endpoint — the API confirms the code matches, is not expired, and has not exceeded the maximum attempt count.

Q: What is the correct TRAI DLT category for OTP SMS in India? OTP SMS belongs to the Service Implicit (SI) category under TRAI's DLT framework — not Transactional (T), which is a common misclassification. The SI category routes OTP messages on a dedicated high-priority queue separate from standard transactional traffic, delivering in 1.2 to 2.1 seconds average across Indian operators. Using Category T for OTP messages routes them through the slower transactional queue and may create classification compliance issues.

Q: How should I secure an OTP SMS API integration? Seven essential security measures: (1) Use a CSPRNG — Python secrets, Node.js crypto.randomInt(), PHP random_int() — never random.randint() or Math.random(). (2) Never store plaintext OTPs — store only the salted SHA-256 hash or use the gateway's Verify API. (3) Rate limit the send endpoint — maximum 3 OTPs per number per 10 minutes. (4) Limit verification attempts — maximum 3 to 5 attempts per OTP before invalidation. (5) Enforce OTP expiry — 5 to 10 minutes for login, 2 to 5 minutes for payment. (6) Never return the OTP in the API response to the client. (7) Invalidate the OTP immediately after successful verification to prevent replay attacks.

Q: What is OTP bombing and how do I prevent it? OTP bombing is an attack where an adversary repeatedly triggers OTP sends to a victim's phone number, causing SMS harassment, SMS credit exhaustion, and potentially locking the legitimate user out by overwhelming their inbox. Prevention requires multi-layer rate limiting: maximum 3 OTP requests per phone number per 10 minutes, maximum 10 OTP requests per IP address per hour, and a mandatory 60-second resend cooldown between consecutive sends to the same number. Techto Networks' OTP SMS API includes API-level rate limiting that can be configured via the otp_config.max_resends and otp_config.resend_cooldown_seconds parameters.

Q: What is the difference between OTP and TOTP? OTP via SMS is a one-time password delivered through an out-of-band channel — the SMS reaches the user's registered phone number, and their possession of that phone provides the verification. TOTP (Time-Based OTP, RFC 6238) is generated by an authenticator app using a shared secret and the current timestamp — no SMS delivery required. TOTP is more secure (not susceptible to SIM swapping) but requires app installation and initial setup, making it less suitable for consumer-facing applications with broad audiences. For most Indian consumer applications, SMS OTP is the appropriate primary method; TOTP can be offered as an advanced option.

Q: How long should an OTP be valid? For login and registration verification: 5 to 10 minutes. For payment authentication: 2 to 5 minutes (RBI recommends maximum 5 minutes for payment OTPs). For password reset: 10 to 15 minutes. For high-security operations: 60 to 120 seconds. Shorter expiry windows reduce the attack surface for interception and replay attacks. The optimal window balances security (shorter is better) against user experience (users in areas with slow SMS delivery need enough time to receive and enter the code).

Q: Can OTP SMS be delivered to DND-registered numbers in India? Yes. OTP SMS is classified as Service Implicit (SI) under TRAI's DLT framework, which is exempt from DND filtering. DND restrictions apply only to promotional SMS (Category P) — which cannot be delivered to DND-registered numbers between 10 AM and 9 PM. Service Implicit and Transactional categories are delivered 24/7 to all numbers including DND-registered ones, because they carry service communications that customers have a legitimate right to receive.

Q: What are the RBI rules for banking OTP SMS in India? RBI's 2024 circular on digital payment security mandates four specific rules for banking authentication OTPs: (1) No URLs of any kind in the OTP message — this includes app store links, deep links, and bit.ly shortened links. (2) Numeric OTP only — alphanumeric codes are not compliant for banking authentication. (3) Maximum 5-minute expiry for payment authentication OTPs. (4) Mandatory "Do not share" warning in the message. Operators enforce the URL prohibition at the SMSC level in 2026 — banking OTP templates containing URLs are silently blocked. Techto Networks provides pre-configured RBI-compliant banking OTP template registration for all financial sector clients.

H2 — Getting Started with Techto Networks' OTP SMS API

H3 — Step 1: Create Your Account and Get API Credentials (2 Minutes)

Register at techtonetworks.com. API credentials are activated immediately — no credit card required for the sandbox environment. Sandbox mode allows full OTP send-verify-webhook flow testing without spending production credits.

H3 — Step 2: Register Your OTP Template on TRAI DLT (48–72 Hours)

Techto Networks' compliance team handles your complete TRAI DLT registration: entity registration, Sender ID approval under the SI category, and OTP template submission. Provide your business documents (GST, PAN, registration certificate) and your intended message template. We review for compliance issues before submission — preventing rejection and wasted time.

H3 — Step 3: Integrate and Test in Sandbox

Use the code examples in this guide to integrate the send and verify endpoints. Test in sandbox with test Template IDs pre-configured — no DLT approval needed in sandbox. Verify: OTP delivery across all four Indian operators, webhook DLR processing, expiry enforcement, rate limiting, and voice fallback triggering.

H3 — Step 4: Go Live

Switch from sandbox to production API credentials. Run the complete security and compliance checklist below before switching production traffic.

H2 — Pre-Launch OTP Integration Checklist

Security:

  • ☐ OTP generator uses CSPRNG (not random/Math.random/rand())

  • ☐ OTPs stored as salted hash (not plaintext) — or using gateway Verify API

  • ☐ OTP invalidated immediately after successful verification

  • ☐ Rate limiting active: 3 OTPs per number per 10 minutes

  • ☐ IP-level rate limiting active: 10 per IP per hour

  • ☐ Resend cooldown enforced: 60 seconds minimum

  • ☐ Verification attempt limit: 3 attempts max before OTP invalidation

  • ☐ OTP not returned in API response to client

  • ☐ Session token issued post-verification (OTP is not the session credential)

TRAI DLT Compliance (India):

  • ☐ OTP template registered under Service Implicit (SI) category

  • ☐ Template contains OTP var, app name var, expiry var, and "Do not share" text

  • ☐ Banking OTP template has NO URLs (RBI mandate)

  • ☐ Banking OTP uses numeric code only

  • ☐ Sender ID registered under SI category (alphanumeric, 6 chars)

  • ☐ Template ID stored in environment variables and passed in every API call

Delivery and UX:

  • ☐ Test OTP delivery on Jio, Airtel, Vodafone Idea, and BSNL numbers

  • ☐ Webhook DLR handler deployed and tested

  • ☐ Voice OTP fallback configured for critical flows

  • ☐ Client-side countdown timer shows OTP expiry to user

  • ☐ Clear error messages: "OTP expired", "Incorrect OTP", "Too many attempts"

  • ☐ WebOTP API auto-fill configured (Android)

  • ☐ Resend button shows only after cooldown period

Error Handling:

  • ☐ DLT_TEMPLATE_MISMATCH error caught and ops alerted (not shown to user)

  • ☐ INSUFFICIENT_CREDITS alert set up

  • ☐ API timeout handled gracefully (10 second timeout + retry)

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page