OTP SMS API: How It Works, Security Best Practices, and Real Code Examples
Updated: 2 days ago
Originally published July 1, 2025. Code samples fully replaced to match TechTo's real, documented API. Reviewed and updated periodically — last reviewed September 2026.
Quick Answer
An OTP SMS API is the programmatic interface your application calls to deliver a one-time password over SMS. The gateway's job stops at delivery — generating the code, storing it securely, and verifying what the user types back in are your application's responsibility, not the API's. This guide covers the full flow, the DLT category question specific to India, security best practices that matter regardless of provider, and working code samples built on TechTo's real HTTP API.
Table of Contents
What an OTP SMS API Actually Does — and Doesn't Do
OTP vs. 2FA vs. TOTP
The Complete Flow: Trigger, Generate, Store, Send, Verify
Generating a Secure OTP
Storing an OTP Safely
Sending the OTP: Real API Code Examples
Checking Delivery Status
Verifying the OTP
DLT Categorization for OTP Messages in India
Security Best Practices
12 Common OTP SMS API Mistakes
Testing Your Integration Before Going Live
Getting Started
Pre-Launch Checklist
Frequently Asked Questions
1. What an OTP SMS API Actually Does — and Doesn't Do
An OTP SMS API is a programmatic interface that delivers a one-time password to a user's mobile number via SMS. That's the whole job: delivery. It's worth being precise about this because a lot of content in this space — including, until this rewrite, our own — implies the API also generates and verifies the code for you, as if OTP were a single hosted service rather than three separate responsibilities split between your application and the gateway.
The real division of labor:
Your application generates the code — a random, unpredictable value your own backend creates.
Your application stores the code — hashed, with an expiry timestamp, tied to the user's session.
The SMS API delivers the code — this is the one piece the gateway actually does.
Your application verifies the code — comparing what the user typed against what you stored, checking expiry and attempt limits.
If a provider's documentation describes a "Verify API" that eliminates the need for you to store anything, that's a real, if less common, product pattern — but confirm it's actually documented before architecting around it, rather than assuming it exists because it would be convenient. TechTo's documented API does not include one; the send/verify split above is the architecture this guide builds around, and it's the safer default assumption for any provider until proven otherwise.
<a name="otp-vs-2fa-vs-totp"></a>
2. OTP vs. 2FA vs. TOTP
These three terms get used interchangeably, but they're distinct:
Term | 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), no SMS involved |
2FA is the process. OTP is one way to implement it. TOTP is a specific OTP variant that doesn't need SMS delivery at all — the code is generated locally on the user's device from a shared secret and the current time. An OTP SMS API is specifically the SMS delivery path for OTP-based authentication.
When SMS OTP makes more sense than TOTP: universal reach — no app installation, no shared secret to manage, works on any phone including feature phones. This is why SMS OTP remains the default for most Indian consumer applications despite TOTP's security advantages (no SIM-swap exposure); TOTP is better offered as an optional advanced setting for users who want it, not the sole authentication path for a broad consumer audience.
3. The Complete Flow: Trigger, Generate, Store, Send, Verify
USER ACTION (login / payment / signup)
│
▼
YOUR APPLICATION BACKEND
├── Validate phone number format
├── Check rate limit (per number, per IP)
├── Generate cryptographically secure OTP
├── Hash + store OTP with expiry
└── Call the SMS API's send endpoint
│
▼
SMS GATEWAY
├── Authenticate the request (token)
├── Validate against the DLT-approved template
├── Route the message (see Section 9 on categorization)
└── Submit to the recipient's operator
│
▼
USER'S PHONE receives the SMS
│
▼
USER ENTERS the code in your app
│
▼
YOUR APPLICATION BACKEND
├── Look up the stored hash for this session
├── Check expiry and attempt count
├── Compare the hash of the entered code
└── Grant or deny access
Every step above except "SMS GATEWAY" happens in your own application. That's the architecture the rest of this guide assumes.
<a name="generation"></a>
4. Generating a Secure OTP
This is the step with the highest security variance in the entire flow, and the difference between secure and insecure often comes down to a single function call.
Insecure — do not use:
# ❌ Python's random module is predictable — do not use for OTPs
import random
otp = str(random.randint(100000, 999999))
// ❌ Math.random() is not cryptographically secure
const otp = Math.floor(Math.random() * 900000 + 100000).toString();
// ❌ PHP's rand() is not cryptographically secure
$otp = rand(100000, 999999);
These use pseudorandom number generators whose internal state can, in principle, be inferred by an attacker who observes enough outputs. There's no reason to accept that risk when a secure alternative is a one-line swap.
Secure — use these instead:
# ✅ Python's secrets module uses OS-level entropy
import secrets
def generate_otp(length: int = 6) -> str:
lower = 10 ** (length - 1)
upper = 10 ** length - 1
return str(secrets.randbelow(upper - lower + 1) + lower)
// ✅ Node.js crypto.randomInt() uses OS-level 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's random_int() uses an 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's SecureRandom uses OS-level entropy
import java.security.SecureRandom;
public class OtpGenerator {
private static final SecureRandom secureRandom = new SecureRandom();
public static String generate(int length) {
int min = (int) Math.pow(10, length - 1);
int max = (int) Math.pow(10, length) - 1;
return String.valueOf(min + secureRandom.nextInt(max - min + 1));
}
}
5. Storing an OTP Safely
Never store the plaintext code. If your database or cache is compromised, every stored plaintext OTP is immediately exploitable. Store a salted hash instead, with an expiry timestamp and an attempt counter:
import hashlib
import secrets
import time
import json
def store_otp(otp: str, phone: str, expiry_seconds: int = 300) -> None:
salt = secrets.token_hex(16)
otp_hash = hashlib.sha256(f"{otp}{salt}".encode()).hexdigest()
record = {
"otp_hash": otp_hash,
"salt": salt,
"expires_at": time.time() + expiry_seconds,
"attempts": 0
}
# Redis is a good fit here — TTL handles expiry automatically
redis_client.setex(f"otp:{phone}", expiry_seconds, json.dumps(record))
6. Sending the OTP: Real API Code Examples
Once the code is generated and stored, your application sends it via TechTo's HTTP API — a token-authenticated GET request. For the full API reference (Tally username/password variant, XML API, error codes), see Message APIs.
Endpoint:
A successful call returns a numeric message ID as plain text — store it to check delivery status afterward. A response in the 101–111 range is an error, not a message ID (see Message APIs — Error Code Reference for the full list).
A note on the route parameter for OTP traffic: the documented route codes are Promotional (1), Transactional (2), Sender ID (3), International (9), and Trans2 (10) — OTP isn't broken out as its own numbered route. Most OTP traffic runs on the Transactional route, but confirm the correct route for OTP-priority delivery with your account setup rather than hardcoding a number, since this affects both delivery speed and DLT categorization (see Section 9).
Python
import requests
API_TOKEN = "YOUR_API_TOKEN"
BASE_URL = "https://godspeed.liveair.co.in/httpapi/tokan/"
def send_otp(sender: str, number: str, otp: str, route: str = "2") -> str:
"""
route: confirm the correct OTP-priority route code with your account setup.
"""
params = {
"token": API_TOKEN,
"sender": sender,
"number": number,
"route": route,
"type": "1", # Text
"sms": f"{otp} is your verification code. Valid for 5 minutes. Do not share this code."
}
response = requests.get(BASE_URL, params=params, timeout=10)
return response.text.strip() # numeric message ID, or a 101–111 error code
Node.js
const axios = require("axios");
const API_TOKEN = "YOUR_API_TOKEN";
const BASE_URL = "https://godspeed.liveair.co.in/httpapi/tokan/";
async function sendOtp(sender, number, otp, route = "2") {
const response = await axios.get(BASE_URL, {
params: {
token: API_TOKEN,
sender,
number,
route,
type: "1",
sms: `${otp} is your verification code. Valid for 5 minutes. Do not share this code.`
},
timeout: 10000
});
return String(response.data).trim();
}
PHP
<?php
function sendOtp(string $sender, string $number, string $otp, string $route = "2"): string {
$params = [
"token" => "YOUR_API_TOKEN",
"sender" => $sender,
"number" => $number,
"route" => $route,
"type" => "1",
"sms" => "{$otp} is your verification code. Valid for 5 minutes. Do not share this code."
];
$url = "https://godspeed.liveair.co.in/httpapi/tokan/?" . http_build_query($params);
return trim(file_get_contents($url));
}
cURL
curl -G "https://godspeed.liveair.co.in/httpapi/tokan/" \
--data-urlencode "token=YOUR_API_TOKEN" \
--data-urlencode "sender=TECHTO" \
--data-urlencode "number=9198XXXXXXX0" \
--data-urlencode "route=2" \
--data-urlencode "type=1" \
--data-urlencode "sms=847291 is your verification code. Valid for 5 minutes. Do not share this code."
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class OtpSender {
private static final String API_TOKEN = "YOUR_API_TOKEN";
private static final String BASE_URL = "https://godspeed.liveair.co.in/httpapi/tokan/";
public static String sendOtp(String sender, String number, String otp, String route) throws Exception {
String message = otp + " is your verification code. Valid for 5 minutes. Do not share this code.";
String url = String.format("%s?token=%s&sender=%s&number=%s&route=%s&type=1&sms=%s",
BASE_URL, enc(API_TOKEN), enc(sender), enc(number), enc(route), enc(message));
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
return response.body().trim();
}
private static String enc(String value) throws Exception {
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
}
}
7. Checking Delivery Status
TechTo's documented API exposes delivery status through a poll-based lookup, not a pushed webhook — your application requests the status using the message ID returned from the send call:
def check_delivery(message_id: str) -> dict:
response = requests.get(BASE_URL, params={"token": API_TOKEN, "messageid": message_id}, timeout=10)
return response.json()
# Example response:
# {
# "messageid": "1758019200321",
# "reports": [
# {"number": "9198XXXXXXX0", "status": "DELIVRD", "time": "2026-09-18 10:15:32"}
# ]
# }
A short delay (a few seconds) after sending before your first check gives the message realistic time to reach the carrier and generate a status — polling immediately in a tight loop won't produce a faster answer. If you need to react to delivery failure in real time (for example, to trigger a fallback channel), build that as a short polling loop with a defined timeout in your own application, since the gateway won't push the update to you.
8. Verifying the OTP
This is entirely your application's responsibility. There's no gateway endpoint to call here — you're comparing what the user typed against what you stored in Section 5:
import time
import json
import hashlib
def verify_otp(phone: str, submitted_code: str) -> dict:
record = redis_client.get(f"otp:{phone}")
if not record:
return {"verified": False, "reason": "OTP_NOT_FOUND_OR_EXPIRED"}
data = json.loads(record)
if time.time() > data["expires_at"]:
redis_client.delete(f"otp:{phone}")
return {"verified": False, "reason": "OTP_EXPIRED"}
if data["attempts"] >= 3:
redis_client.delete(f"otp:{phone}")
return {"verified": False, "reason": "MAX_ATTEMPTS_EXCEEDED"}
data["attempts"] += 1
redis_client.setex(f"otp:{phone}", int(data["expires_at"] - time.time()), json.dumps(data))
input_hash = hashlib.sha256(f"{submitted_code}{data['salt']}".encode()).hexdigest()
if input_hash != data["otp_hash"]:
return {"verified": False, "reason": "OTP_MISMATCH"}
redis_client.delete(f"otp:{phone}") # single-use — invalidate on success
return {"verified": True}
For a deeper look at the full lifecycle — expiry windows, resend cooldowns, and fallback-channel design — see our implementation guide: OTP Sender Online: How OTP SMS Sending Actually Works. For a shorter, complementary take on the sending side specifically, see OTP Sender.
9. DLT Categorization for OTP Messages in India
This is a genuinely nuanced point, and worth getting right rather than repeating an oversimplified rule. India's DLT framework recognizes four commercial SMS categories: Promotional, Transactional, Service Implicit, and Service Explicit (the last of these was substantially discontinued under TRAI's February 2025 amendment). Where OTP fits within that isn't uniformly described across providers' own documentation:
Most DLT registration guidance recommends Service Implicit for OTP templates. The reasoning: OTP delivery relies on implied consent (the user initiated the login or transaction), the category is exempt from DND restrictions, and it's the category several major aggregators explicitly instruct customers to use for OTP.
Some providers' documentation reserves "Transactional" specifically for bank-issued OTPs, treating Service Implicit as the category for OTP and service messaging from everyone else.
In practice, both approaches route around DND restrictions and deliver 24/7 — the DND-exemption behavior that matters for OTP delivery holds either way.
The practical takeaway: register your OTP templates under whichever category your specific DLT platform and provider document as correct for OTP traffic — don't assume "Transactional" is automatically right just because OTP feels transactional in a colloquial sense, and don't assume "Service Implicit" is universally required either. Confirm directly with your provider's compliance team, since template rejections and DND-scrubbing failures are the direct cost of getting this wrong.
A concrete illustration of why this matters in practice: a business that registers its OTP template under the wrong category can see delivery work perfectly in testing — because test numbers are rarely DND-registered — and then fail silently in production the moment a real user with a DND-registered number tries to log in. The API call still returns a message ID, the gateway still accepts the request, but the carrier's own scrubbing layer drops the message before it reaches the handset. This is exactly the kind of failure that looks like "random" delivery problems from the application layer, when it's actually a fixed, diagnosable categorization issue. For the complete regulatory framework — PE-TM chaining, header registration, and the 2024/2025 TRAI amendments — see Bulk SMS India: DLT Compliance, Pricing, and APIs.
A note on banking OTP specifically: RBI guidance for digital payment security has pushed banks toward stricter OTP template rules — avoiding URLs in payment-authentication OTP content, using numeric-only codes, and shorter expiry windows (commonly cited around 3–5 minutes for payment authorization). If you're building payment-adjacent OTP flows, confirm current RBI guidance and your bank partner's specific requirements directly rather than relying on a general SMS provider's summary of banking rules, since this is a regulatory area that sits partly outside standard TRAI/DLT SMS compliance.
10. Security Best Practices
OTP Expiry Windows
Use Case | Recommended Expiry |
Login / signup verification | 5–10 minutes |
Payment authentication | 2–5 minutes |
Password reset | 10–15 minutes |
High-security actions (admin access, account deletion) | 60–120 seconds |
Shorter windows reduce the exposure period if a code is intercepted; longer windows reduce legitimate user drop-off. Enforce the expiry server-side — the message text saying "valid for 5 minutes" means nothing if your application doesn't actually check it.
Rate Limiting — Preventing OTP Bombing
OTP bombing (also sometimes called SMS pumping) is an attack where a bad actor triggers large volumes of OTP requests against numbers they don't control — for harassment, to exhaust your credits, or in some cases as a fraud scheme against interconnect billing. Defend with layered limits:
Per phone number: a small number of requests per 10-minute window (commonly 3)
Per IP address: a broader hourly cap (commonly 10)
A resend cooldown between consecutive requests to the same number (commonly 60 seconds)
Attempt Limiting on Verification
Without a cap on verification attempts, an attacker could brute-force a 6-digit code given enough tries. Limit to 3–5 attempts before invalidating the code entirely and requiring a fresh request.
Never Return the OTP in the API Response to Your Client
The client should only ever receive confirmation that an OTP was sent — never the code itself in the response body. The out-of-band SMS channel is the entire point of the security model; putting the code in an API response to the same client defeats it.
Invalidate on Success
Once a code verifies successfully, delete it immediately. A code that remains "valid" after use is a real, avoidable replay risk.
WebOTP API for Autofill (Android)
The WebOTP API is a browser-level standard that lets a correctly formatted SMS be automatically read and filled into the input field on supporting Android browsers, removing the manual copy-paste step. It requires a specific message format (an app-linked domain-bound hash appended to the message) — check current browser support and format requirements before depending on it for your primary UX rather than as a progressive enhancement.
Bind the OTP to a Session, Not Just a Phone Number
Storing an OTP keyed only by phone number can create a subtle race condition: if a user requests a second OTP before verifying the first (a second tab, a page refresh), which code is "current"? Bind each OTP to a specific session or request ID rather than the bare phone number, and invalidate any prior outstanding code the moment a new one is generated for the same user — this avoids a state where an old, still-technically-valid code from an earlier request can be used after the user believes they've moved on to a new one.
Log OTP Events for Fraud Investigation
When a user disputes an unauthorized login, your security team needs to reconstruct what happened: when the OTP was generated, when it was delivered, when (and from what IP and device) it was verified, and how many attempts were made. Log timestamps for each stage and the hash of the code — never the plaintext — so this history is available after the fact without having stored anything an attacker could exploit if your logs are ever exposed.
11. 12 Common OTP SMS API Mistakes
Using a non-cryptographic random generator (random.randint(), Math.random(), rand()) — always use the CSPRNG equivalent instead.
Storing plaintext OTPs. Store a salted hash, or nothing at all if the code is ephemeral in a cache with automatic expiry.
No rate limiting on the send endpoint — leaves you exposed to OTP bombing and credit exhaustion.
No attempt limiting on verification — leaves a 6-digit code brute-forceable given enough tries.
Excessively long expiry windows. A 30-minute OTP is functionally a static password for that window.
Triggering an OTP on every page load instead of on an explicit user action — inflates cost and trains users to ignore OTPs.
Returning the OTP in the API response to the client — defeats the out-of-band security model entirely.
Allowing OTP reuse after successful verification — invalidate immediately on success.
Assuming a gateway-side Verify API exists without confirming it's documented — as covered in Section 1, don't architect around a feature you haven't confirmed is real.
Guessing the DLT category instead of confirming it. As covered in Section 9, "OTP feels transactional" isn't the same as knowing which category your specific provider requires.
Using one OTP template for both banking and non-banking flows when RBI-adjacent rules (no URLs, numeric-only) apply to one but not the other — maintain separate templates.
Not logging OTP events for fraud investigation. When a user reports an unauthorized login, you need to answer when the OTP was generated, sent, and verified, and from what IP — log timestamps and the hash (never the plaintext code).
12. Testing Your Integration Before Going Live
Before flipping real traffic onto your new OTP flow, a short structured test catches most of the problems that otherwise surface as production incidents:
Send to numbers on every major operator, not just whichever carrier your own test phone happens to use — Jio, Airtel, Vodafone Idea, and BSNL can behave differently under the same template and route, and BSNL in particular is where aggregated-routing weaknesses tend to show up first.
Include at least one DND-registered test number if you can arrange one, specifically to confirm your OTP category is correctly configured to bypass DND filtering — this is the single most common category-misconfiguration failure mode described in Section 9, and it's far better to catch it in testing than from a support ticket.
Deliberately trigger a few failure conditions — an invalid number, a rate-limit breach, a message sent outside an allowed route's hours if applicable — and confirm your application surfaces a clear, specific error rather than a generic failure state.
Verify your expiry and attempt-limit logic under real timing, not just by reading the code — request an OTP, wait past the expiry window, and confirm the verify step actually rejects it; then request a fresh one and deliberately enter the wrong code the maximum number of times to confirm lockout behavior works as designed.
Time the full round trip from your send call to the user's handset under normal conditions, so you have a real baseline to compare against if delivery speed later seems to degrade.
13. Getting Started
Register your OTP template on the DLT platform under the category your provider's compliance team confirms is correct for your use case (Section 9).
Test the template manually — confirm the exact approved wording, sender ID, and route before writing integration code, since a mismatch here causes silent delivery failures that are hard to debug from application logs alone.
Build the generate/store/verify logic in your application (Sections 4, 5, 8) — this is your responsibility regardless of provider.
Integrate the send call using the code samples in Section 6.
Add rate limiting and attempt caps (Section 10) before going live — this isn't optional hardening for a later release.
Confirm the correct route for OTP-priority delivery with your account setup rather than assuming a route number.
<a name="checklist"></a>
14. Pre-Launch Checklist
Security:
OTP generated with a CSPRNG (not random/Math.random/rand)
OTP stored as a salted hash, never plaintext
OTP invalidated immediately after successful verification
Rate limiting active: per-number and per-IP
Resend cooldown enforced
Verification attempt limit enforced (3–5 attempts)
OTP never returned in the API response to the client
DLT compliance (India):
OTP template registered and confirmed active on the DLT platform
Correct category confirmed directly with your provider (Section 9), not assumed
Template content in your code exactly matches the approved template text
Separate templates maintained for banking vs. non-banking OTP flows if applicable
Delivery and integration:
Test sends confirmed on multiple operators, not just one
Delivery-status polling implemented and tested (Section 7)
Client-side countdown shown to the user for OTP expiry
Clear, distinct error messages for expired, incorrect, and rate-limited states
15. Frequently Asked Questions
1. Does the OTP SMS API verify the code the user enters?
No TechTo's documented API only handles delivery. Generating, storing, and verifying the code are your application's responsibility. See Section 1 and Section 8.
2. What DLT category should OTP messages use in India?
Most guidance points to Service Implicit, though some providers reserve "Transactional" specifically for bank OTPs. Confirm the correct category with your specific provider's compliance team rather than assuming — see Section 9.
3. How do I know if my OTP was actually delivered, not just accepted by the API?
Poll the Delivery Report endpoint using the message ID returned from your send call (Section 7). A successful API response only confirms the gateway accepted the request, not that the message reached the handset.
4. What's the difference between OTP and TOTP?
OTP via SMS relies on an out-of-band channel — possession of the registered phone number is the proof. TOTP is generated locally by an authenticator app from a shared secret and doesn't require SMS delivery at all. TOTP is more resistant to SIM-swap attacks but requires app setup.
5. How long should an OTP remain valid?
3–10 minutes is typical, shorter for payment authorization. Enforce the expiry server-side regardless of what the message text says.
6. What is OTP bombing and how do I prevent it?
It's an attack where OTP requests are triggered repeatedly against a number the attacker doesn't control. Prevent it with layered rate limits — per number, per IP, and a resend cooldown between requests.
7. Can OTP SMS reach DND-registered numbers in India?
Yes, when correctly categorized (Service Implicit or the equivalent category your provider uses for OTP) — DND restrictions apply to promotional content, not properly classified OTP/service traffic.
8. Should I build my own OTP verification, or look for a provider with a hosted verify API?
Building it yourself (as shown in Section 8) is the safer default assumption and gives you full control over expiry, attempts, and logging. If a provider genuinely offers a documented hosted verification service, evaluate it on its own terms — but don't assume one exists without confirming it in the provider's actual documentation.
9. Which providers should I consider for OTP delivery in India?
See our practical evaluation framework in OTP Service Providers India, our ranked comparison in OTP Service Providers, and our implementation-focused guide in OTP Sender Online.
10. Do OTP messages need a different sender ID than promotional messages?
Yes promotional and transactional/service headers are registered separately under India's DLT framework, and an OTP template must use a header registered for the correct category, not a promotional one.
11. How should I test my OTP integration before launch?
Send to test numbers across all major operators, include a DND-registered number if possible to confirm your category configuration bypasses DND correctly, and deliberately trigger expiry and max-attempt scenarios to confirm your application handles them as designed. See Section 12 for the full pre-launch testing checklist.
12. What happens if my OTP API call succeeds but the user never receives the message?
This is almost always a DLT categorization or template-matching issue rather than a networking problem the gateway accepted the request, but the carrier's scrubbing layer silently dropped it because the content, category, or template didn't match what's registered. Poll the delivery status endpoint (Section 7) to confirm actual delivery rather than trusting the send response alone.
13. Is it safe to reuse the same OTP if a user requests a resend within the expiry window?
No generate a fresh code on every resend request and reset the expiry timer. Reusing the same code across resends only extends the window an intercepted code stays valid, with no benefit to the user experience.




Comments