SMS-based one-time passwords remain the most widely deployed second factor for user authentication. Despite ongoing debate about their security relative to authenticator apps or hardware keys, SMS OTP is used by billions of services because it works on every phone, requires no app installation, and has near-universal user familiarity. This guide covers how to implement SMS OTP correctly -- the generation algorithm, the delivery pipeline, the security considerations, and the cost realities.
What Is SMS OTP Authentication?
SMS OTP authentication sends a short numeric or alphanumeric code to a user's phone number via text message. The user enters that code into your application to prove they control the phone number. This serves two purposes:
- Two-factor authentication (2FA) -- Adding a second factor ("something you have" -- the phone) on top of a password ("something you know") to protect account login.
- Phone number verification -- Confirming that a user actually owns the phone number they provided during registration, onboarding, or a transaction.
The basic flow is straightforward: your application generates a code, sends it via SMS, and then verifies the code when the user submits it. The complexity is in doing each of these steps correctly and at scale.
OTP Generation Methods
There are three common approaches to generating OTP codes. Each has different characteristics for security, statelessness, and implementation complexity.
Random OTP (Most Common for SMS)
The simplest approach: generate a random number, store it server-side with an expiry timestamp, and send it to the user. When the user submits a code, compare it against the stored value.
# Python example -- random OTP generation
import secrets
import time
def generate_otp(length=6):
"""Generate a cryptographically random numeric OTP."""
# Use secrets module, NOT random -- random is not cryptographically secure
code = ''.join([str(secrets.randbelow(10)) for _ in range(length)])
return code
def create_otp_record(phone_number, ttl_seconds=300):
"""Create and store an OTP with expiry."""
code = generate_otp(6)
record = {
'phone': phone_number,
'code': code,
'expires_at': time.time() + ttl_seconds,
'attempts': 0,
'max_attempts': 3
}
# Store in Redis with TTL for automatic cleanup
# redis.setex(f"otp:{phone_number}", ttl_seconds, json.dumps(record))
return code
Critical detail: use secrets.randbelow() or random_int() (PHP) or crypto.randomInt() (Node.js) -- cryptographically secure random number generators. Never use Math.random(), rand(), or random.randint() for security-sensitive code generation. These functions use pseudo-random number generators that can be predicted if an attacker observes enough outputs.
TOTP (Time-Based One-Time Password)
TOTP, defined in RFC 6238, generates a code from a shared secret and the current time. This is what authenticator apps (Google Authenticator, Authy) use. The code changes every 30 seconds, and both sides can compute it independently without communication.
TOTP is rarely used for SMS OTP because it requires a pre-shared secret (which the user would need to set up initially), and the 30-second window does not align well with SMS delivery latency, which can range from 2 to 60 seconds depending on the route. If the SMS takes 35 seconds to arrive, the code may have already expired. You can extend the window, but that weakens the security guarantee.
HOTP (HMAC-Based One-Time Password)
HOTP, defined in RFC 4226, generates a code from a shared secret and a counter that increments with each use. Like TOTP, it requires a pre-shared secret. HOTP is used in some hardware token implementations but is uncommon for SMS OTP for the same reason as TOTP -- the pre-shared secret requirement adds setup complexity.
For SMS OTP, the random approach is almost always the right choice. It requires no pre-shared state, works immediately, and the server-side storage is trivial (a Redis key with a TTL).
OTP Message Format Best Practices
The message content matters more than you might think. Poorly formatted OTP messages reduce verification rates, confuse users, and can trigger carrier spam filters.
Recommended Format
Your ACME verification code is: 847291. It expires in 5 minutes. Do not share this code.
Key elements of a good OTP message:
- Identify the sender -- Include your brand name. "Your verification code" without context is suspicious.
- State the purpose -- "verification code" or "login code" tells the user why they received it.
- Include the code prominently -- Put it where it is easy to see. Many phones show the first line of an SMS as a preview.
- State the expiry -- "Expires in 5 minutes" sets expectations and reduces support tickets from users who wait too long.
- Warn against sharing -- "Do not share this code" is a simple defense against social engineering attacks where a fraudster asks the victim to read out the code.
- Keep it under 160 characters -- A single GSM-7 SMS segment. Multi-segment messages cost more, arrive slower, and can arrive out of order on some networks.
Support for Autofill
Modern mobile operating systems can automatically detect OTP codes in SMS messages and offer to autofill them into the requesting app. To support this:
- Android -- Use the SMS Retriever API format. Append a hash of your app's package name to the message:
FA+9qCX9VSu. The OS matches this hash and auto-fills the code without requiring SMS read permission. - iOS -- Apple's autofill works heuristically. Place the code on its own line or after a recognizable prefix. The domain-bound format
@yourdomain.com #847291enables the strongest binding. - Web -- Use the WebOTP API with the format:
Your code is: 847291\n\n@yourdomain.com #847291. The last line tells the browser which domain can receive this code.
Delivery Infrastructure
OTP messages have different requirements than bulk marketing SMS. They must arrive quickly (under 10 seconds ideally), reliably (failure means the user cannot log in), and they tend to be sent one at a time rather than in batches.
SMPP vs HTTP API
Two primary protocols are used for sending SMS programmatically:
- SMPP (Short Message Peer-to-Peer) -- A binary protocol with persistent TCP connections. SMPP is the industry standard for high-volume SMS delivery. It supports windowing (multiple outstanding messages), delivery receipts, and fine-grained error codes. If you are sending more than a few hundred OTP messages per minute, SMPP is the better choice for latency and throughput. MOBITELSMS provides direct SMPP connectivity for this use case.
- HTTP API -- REST-based submission, typically one API call per message. Simpler to implement but adds HTTP overhead per message. Good for lower volumes or when you want to avoid managing SMPP connections.
Delivery Priorities
OTP messages should be sent on high-priority routes with direct carrier connections. Key considerations:
- Route quality -- Use direct-to-carrier routes, not grey routes or aggregator chains. Grey routes have unpredictable latency and can filter OTP messages.
- Sender ID -- Use a consistent sender ID (short code or alphanumeric) that users recognize. Rotating sender IDs confuses users and can trigger spam filters.
- Fallback channels -- If SMS delivery fails or is delayed, offer voice OTP (an automated call that reads the code) as a fallback. Email OTP is another option but is less reliable as a phone-number verification method.
- Delivery receipts -- Monitor DLR status. If a message is not delivered within 30 seconds, trigger a retry on a different route or offer the user a fallback option.
Expiry Times and Retry Logic
OTP codes should have a limited lifetime. The expiry window balances security (shorter is better) against usability (the user needs time to receive and enter the code).
- Recommended expiry: 5 minutes -- This gives enough time for delayed SMS delivery and slow typists while limiting the window for interception.
- Maximum expiry: 10 minutes -- Beyond this, the security benefit of a one-time code diminishes significantly.
- Minimum expiry: 2 minutes -- Some high-security applications use very short windows, but this creates poor user experience if SMS delivery takes more than a few seconds.
Retry Logic
Users will request new codes if the first one does not arrive quickly. Your retry logic should:
- Invalidate the previous code -- When a user requests a new OTP, the old one should be immediately invalidated. Do not allow both to be valid simultaneously.
- Rate limit resend requests -- Allow a maximum of 3-5 resends per phone number per hour. After that, lock the number out temporarily and suggest an alternative verification method.
- Introduce increasing delays -- First resend available after 30 seconds, second after 60 seconds, third after 120 seconds. This discourages rapid-fire requests and gives the SMS system time to deliver.
- Try a different route on retry -- If the first delivery attempt appears to have failed (no DLR within 15 seconds), route the retry through a different carrier path.
Rate Limiting and Abuse Prevention
OTP endpoints are prime targets for abuse. Without rate limiting, an attacker can use your OTP system to harass phone numbers with unwanted messages (SMS bombing), drain your SMS budget, or attempt to brute-force codes.
Rate Limits to Implement
- Per phone number -- Maximum 5 OTP requests per phone number per hour. Maximum 10 per day. These limits prevent SMS bombing and reduce costs from repeated requests.
- Per IP address -- Maximum 10 OTP requests per IP per hour. This catches automated scripts targeting multiple phone numbers from a single source.
- Per account -- If the user is logged in and adding a new phone number, limit to 3 verification attempts per session.
- Global -- Set an overall OTP volume cap for your system. If your normal OTP volume is 1,000/hour and it suddenly spikes to 50,000/hour, that is an attack. Trigger alerts and automatic throttling.
Verification Attempt Limits
Limit the number of incorrect code submissions. A 6-digit numeric code has 1,000,000 possible values. With unlimited attempts, brute force is trivial. With 3 attempts, it is impossible.
# PHP example -- OTP verification with attempt limiting
function verify_otp(string $phone, string $submitted_code): bool {
$key = "otp:" . $phone;
$record = json_decode(redis()->get($key), true);
if (!$record) return false; // No OTP exists
if (time() > $record['expires_at']) {
redis()->del($key);
return false; // Expired
}
if ($record['attempts'] >= $record['max_attempts']) {
redis()->del($key);
return false; // Too many attempts
}
$record['attempts']++;
redis()->setex($key, $record['expires_at'] - time(), json_encode($record));
if (hash_equals($record['code'], $submitted_code)) {
redis()->del($key); // One-time use: delete after success
return true;
}
return false;
}
Note the use of hash_equals() for timing-safe comparison. A regular string comparison (== or ===) can leak information about how many characters matched through timing side channels. While this is harder to exploit for short numeric codes than for passwords, it is a good practice to follow.
SIM Swap Fraud Mitigation
SIM swap attacks are the most significant threat to SMS OTP security. In a SIM swap, an attacker convinces a mobile carrier to transfer the victim's phone number to a new SIM card controlled by the attacker. All subsequent SMS messages -- including OTP codes -- go to the attacker's device.
Detection Strategies
- SIM swap detection APIs -- Several carriers and third-party providers offer APIs that report whether a phone number's SIM was recently changed. If the SIM was swapped within the last 24-72 hours, block SMS OTP and require an alternative verification method.
- Device binding -- Track the device associated with a phone number. If the device changes (detected via device fingerprinting or app-level attestation), escalate to a higher-assurance verification method.
- Behavioral signals -- A login attempt from a new IP/device/location combined with a SIM change is high-risk. Use these signals together rather than relying on any single indicator.
- Network-level checks -- HLR lookups can reveal whether a number is currently active and on its home network. Sudden changes in IMSI (subscriber identity) can indicate a SIM swap. The MOBITELSMS MNP/LNP service can be used for number status verification.
Fallback Channels
For high-security applications (banking, cryptocurrency exchanges), do not rely solely on SMS OTP. Offer and encourage:
- Authenticator apps -- TOTP via Google Authenticator, Authy, or Microsoft Authenticator. Not vulnerable to SIM swap.
- Hardware security keys -- FIDO2/WebAuthn with a YubiKey or similar device. The strongest widely available second factor.
- Push notifications -- App-based push verification that binds to the device, not the phone number.
- Voice OTP -- An automated call that reads the code. Slightly harder to intercept than SMS but still vulnerable to call forwarding attacks.
Regulatory Requirements
SMS OTP implementations must comply with several regulatory frameworks depending on your market:
- PSD2 (Europe) -- The Payment Services Directive 2 requires Strong Customer Authentication (SCA) for electronic payments. SMS OTP qualifies as one factor, but PSD2 requires that the authentication code be dynamically linked to the specific transaction amount and payee. Generic codes are not compliant.
- NIST SP 800-63B (United States) -- NIST guidelines classify SMS OTP as a "restricted" authenticator, meaning it is acceptable for many use cases but agencies should plan migration to stronger methods. The guidance specifically calls out the risk of SS7 interception and SIM swap.
- TCPA / 10DLC (United States) -- OTP messages are generally classified as transactional, not marketing, and do not require prior express consent under TCPA (since the user is actively requesting the code). However, they still need to be sent through registered 10DLC campaigns for long code delivery. See our SMS campaign management for 10DLC compliance.
- GDPR (Europe) -- Phone numbers are personal data. Store OTP records with minimal data, apply appropriate TTLs, and ensure your OTP infrastructure complies with data residency requirements.
Cost Optimization
OTP messages are typically small (under 160 characters, single segment) and transactional, but at scale they represent a significant messaging cost. Strategies for managing OTP costs:
- Domestic vs. international routing -- International OTP delivery can cost 5-20x more than domestic. If your users are concentrated in specific countries, establish direct carrier connections in those markets rather than routing through international aggregators.
- Reduce unnecessary sends -- Implement smart triggering. If a user just verified their phone number 5 minutes ago from the same device, do not require another OTP. Session management and device recognition reduce OTP volume significantly.
- Prevent pumping fraud -- Artificially Inflated Traffic (AIT) or "SMS pumping" occurs when attackers trigger OTP sends to premium-rate numbers or numbers they control in revenue-sharing arrangements. Rate limiting per phone number prefix (country code + MNO code) and monitoring for unusual geographic patterns are essential defenses.
- Channel fallback -- For users with your app installed, use push notifications or in-app verification instead of SMS. SMS should be the fallback, not the default, for users who have a richer channel available.
- Batch pricing -- Negotiate volume-based pricing with your SMS provider. OTP traffic is predictable and steady, which carriers value for capacity planning.
SMS OTP vs. Authenticator Apps
The security community has largely moved toward recommending authenticator apps over SMS OTP. The comparison:
- Security -- Authenticator apps are immune to SIM swap, SS7 interception, and SMS delivery failures. SMS OTP is vulnerable to all three. Advantage: authenticator apps.
- Usability -- SMS OTP requires no setup, works on any phone (including feature phones), and is universally understood. Authenticator apps require installation, setup, and backup planning. Advantage: SMS OTP.
- Reliability -- SMS depends on carrier networks and can fail due to coverage, congestion, or routing issues. Authenticator apps work offline. Advantage: authenticator apps.
- Cost -- SMS OTP costs money per message. Authenticator apps have zero per-authentication cost. Advantage: authenticator apps.
- Reach -- 100% of mobile phone users can receive SMS. Only smartphone users with an installed app can use authenticator apps. For services targeting developing markets or feature phone users, SMS is the only viable option. Advantage: SMS OTP.
The practical conclusion: offer authenticator apps as the recommended option, but always provide SMS OTP as a fallback. For phone number verification (as opposed to login 2FA), SMS remains the standard because the purpose is to verify the number itself.
Implementation Checklist
- Use cryptographically secure random number generation for codes
- Generate 6-digit codes (minimum) -- 4-digit codes are too easy to brute-force even with attempt limits
- Set expiry to 5 minutes
- Limit verification attempts to 3 per code
- Invalidate the code immediately after successful verification
- Rate limit OTP requests per phone number, per IP, and globally
- Use timing-safe comparison for code verification
- Monitor for SIM swap indicators on high-value accounts
- Include sender identification, expiry notice, and anti-sharing warning in the message
- Support autofill formats for Android, iOS, and web
- Implement delivery receipt monitoring with automatic route failover
- Log OTP events for fraud analysis (but never log the code itself)
SMS OTP is not going away anytime soon. Implemented correctly -- with proper code generation, rate limiting, fraud detection, and fallback channels -- it provides a practical and effective layer of account security that works for virtually every user on every device.