OTP vs 2FA — the same SMS, different product surfaces
One-time password (OTP) is the umbrella term for any single-use code delivered via SMS, email, or push. It encompasses four primary use cases:
- Passwordless login: User enters email → receives magic code → enters code → authenticated without password.
- Two-factor authentication (2FA): User enters password → receives second-factor OTP → enters code → gains access. This is the subset most people think of as "OTP".
- Transaction verification: User initiates high-risk action (transfer $500, change email) → receives confirmation code → enters code to confirm.
- Account recovery: User resets password or recovers account → receives multi-digit code → enters code to unlock reset flow.
The distinction matters architecturally: passwordless login and 2FA happen on the critical path (user is waiting), so latency is paramount. Recovery codes can tolerate longer validity windows (10 minutes vs. 30 seconds) because they're lower-frequency. All are OTP; 2FA is one specific flavor.
smsroute treats all four uniformly via the same /v1/messages endpoint. The "otp" traffic class is optional but recommended for routing optimization.
OTP message anatomy — code length, expiry, idempotency
A production OTP message has five moving parts:
1. Code length
6-digit codes are the modal choice: short enough to type in <10 seconds, long enough to resist brute force (1 million combinations). Some apps use 4 digits for ultra-high-frequency flows (e.g., per-API-call auth) or 8 digits for high-security recovery. Longer codes reduce UX friction (fewer accidental failures) but increase user frustration (harder to remember).
Recommended: 6 digits for 2FA and passwordless login; 8 digits for account recovery.
2. Expiry window
Transactional OTPs (passwordless, 2FA, transaction confirm): 30–60 seconds. This window is short enough to prevent code reuse but long enough for network latency and user reaction time. If a user doesn't receive the SMS within 30 seconds, they retry.
Recovery OTPs (password reset, account unlock): 5–10 minutes. Users may not check their phone immediately, and recovery is lower-risk (they've already passed initial verification).
Implementation: Store the code, expiry timestamp, and attempt count in your backend. On each submission, verify the code matches, check that now < expiry, and increment attempt count. After 3 failed attempts, lock the user out and require a new code.
3. Rate limiting per user
Enforce a resend limit: maximum 3 OTP sends per user per 10 minutes. This prevents attackers from spamming a victim's phone and reduces your SMS spend. Track send count by user ID or phone number, not by IP (VPN users may share IPs).
When a user hits the limit, return a 429 (Too Many Requests) to the browser, not to smsroute. smsroute should still send the SMS to the user's phone if you've rate-limited your frontend—i.e., don't rely on smsroute's rate limiting as your primary gate.
4. Idempotency keys to prevent duplicate sends
Network flakiness is real: a user clicks "resend code" and both the first and second request reach your backend and smsroute. Without deduplication, two different codes are active simultaneously, and whichever arrives second invalidates the first, locking the user out.
Solution: generate a unique idempotency_key for each send attempt (e.g., sha256(user_id + timestamp + 1)) and pass it to smsroute. smsroute returns the same message_id and code for duplicate keys within a 24-hour window. Your frontend can safely retry without fear of generating a new code.
5. Message body format
Keep OTP messages short and locale-aware. A minimal message: "Your code: 123456. Valid 10 minutes."
For iOS autofill, prepend the domain hash: "@example.com #123456". This tells iOS to auto-populate the code in the message-reply field.
For Android, use Android SafetyNet Attestation or domain-bound one-time codes to link the code cryptographically to your app domain.
Global delivery — UCS-2, character sets, anti-phishing preambles
Delivering OTP across 149 countries requires careful attention to character encoding and carrier quirks.
Character encoding: GSM-7 vs. UCS-2
SMS supports two standard character sets:
- GSM-7: 7-bit encoding for Latin alphabet, digits, and common punctuation. Fits 160 characters per SMS segment. Lowest cost.
- UCS-2: 2-byte Unicode encoding for any script (Cyrillic, Arabic, CJK, Devanagari, etc.). Fits 70 characters per segment. Higher cost due to longer message length.
smsroute automatically detects the script in your message body and chooses the optimal encoding. If you send "Your code: 123456" (ASCII), it routes as GSM-7 (1 segment, cheapest). If you send "Ваш код: 123456" (Cyrillic), it routes as UCS-2 (1 segment, ~2x cost per character).
For multi-language OTP, compose in the user's locale but keep the code itself in digits (which are GSM-7 compatible). Example: "你的验证码是: 123456" (UCS-2) vs. "Your code: 123456" (GSM-7).
iOS autofill and Android domain binding
iOS: If your message begins with "@yourdomain.com #", iOS Messages will parse the code (digits after #) and offer to autofill it. This requires the domain to be verified in your app's Info.plist or AppDelegate configuration.
Example message: "@example.com #123456 Your verification code." iOS extracts "123456" and pre-fills the input field.
Android: Use the User Consent API (Google Play Services) or AppSignatureHelper to bind the code to your app domain. The message must include a hash of your app's signing certificate. Android then auto-reads the code without user intervention.
Anti-phishing preambles
Some carriers (especially in EU) require explicit context to prevent phishing. If your OTP message doesn't clearly state the action or service, carriers may mark it as spam. Best practices:
- Always include the service name: "Google: 123456 is your verification code."
- For high-value transactions, add context: "Confirm: Transfer €500 to Alice. Code: 123456."
- Never ask the user to share the code in the message itself (attackers impersonate support and trick users into sharing codes).
Common pitfalls and how smsroute handles them
Pitfall 1: Race condition on resend (duplicate code)
Problem: User clicks "resend code" twice in quick succession. Both requests reach your backend and smsroute. Two different OTP codes are generated and sent. User enters the first code successfully, which invalidates the code server-side. Second SMS arrives with stale code → user sees "invalid code" → friction and support escalation.
smsroute solution: Use idempotency_key. Include a deterministic key (e.g., sha256(user_id + send_attempt_number)) in every request. smsroute caches the key for 24 hours. Duplicate requests return the same message_id and code. Your backend can safely retry without generating a new code.
Pitfall 2: User enumeration via delivery receipts
Problem: An attacker calls your OTP endpoint with a list of phone numbers. Your backend returns "delivery success" for registered users and "delivery failed" for non-existent users. Attacker builds a list of valid phone numbers without needing to crack any OTP.
Solution: Always return HTTP 200 (success) to the client, regardless of delivery status. Store the delivery status in your backend database, but don't leak it to the unauthenticated requester. Reveal delivery status only to the authenticated user (via their account dashboard) or to support staff.
Pitfall 3: SS7 interception at the carrier level
Problem: SMS is not end-to-end encrypted. Sophisticated attackers with access to telecom infrastructure (or corrupt insiders) can intercept SMS between the carrier's network and the user's phone. This is the "SIM swap" attack.
Mitigation: For high-security flows (banking, cryptocurrency, admin access), use push notifications as the primary OTP delivery method with SMS as fallback. Require additional verification (biometric, security questions) before accepting an OTP. Implement step-up authentication for sensitive actions.
Pitfall 4: Code validation failures on typo or timing
Problem: User types the code correctly but makes a typo, or the code expires between SMS arrival and form submission. After 3 failed attempts, the user is locked out and must request a new code (generating more SMS cost for you).
Solution: Allow 3–5 failed attempts before lockout. Use a sliding expiry window: if code expires, let the user request a new one, but reuse the old code for 10 more seconds (grace period) to account for clock skew. Log all failures for security audit.
Pitfall 5: Long latency on peak load
Problem: Your app goes viral, OTP volume spikes 10x overnight. smsroute's queue backs up, and some OTPs arrive 60+ seconds late, causing user frustration and timeouts.
smsroute's guarantee: 99.9% uptime and sub-second latency (p99 <500ms) because we don't overload our infrastructure and maintain direct peering with tier-1 carriers. From $0.004/SMS across 149 countries with 99% tier-1 delivery rates. We scale horizontally to handle traffic spikes without degrading latency.
Sandbox mode for OTP testing
Before deploying to production, test your OTP flow end-to-end. smsroute provides sandbox mode: use a test API key (prefix sk_test_) to send OTPs without triggering real SMS or billing.
How it works: Requests with a test API key return instantly with a deterministic code based on the destination phone number.
- Send to any test number (e.g., +1234567890) → always returns code "123456".
- No SMS actually sent to the phone.
- No charges incurred.
- Useful for CI/CD pipelines, integration tests, and staging environments.
Switching to production: Swap the test API key for a production key (prefix sk_live_). All other code remains the same. Real SMS are now sent and billed.
Code examples — curl, Python, Node.js
cURL
curl -X POST https://api.smsroute.cc/v1/messages \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+33612345678",
"body": "@example.com #123456 Your verification code. Valid 10 minutes.",
"traffic_class": "otp",
"idempotency_key": "user_123_send_1"
}'
Python (requests library)
import requests
import hashlib
import time
def send_otp(phone_number, code):
api_key = "sk_live_YOUR_API_KEY"
# Generate idempotency key to prevent duplicate sends on retry
idempotency_key = hashlib.sha256(
f"{phone_number}_{int(time.time())}".encode()
).hexdigest()
payload = {
"to": phone_number,
"body": f"@example.com #{code} Your verification code. Valid 10 minutes.",
"traffic_class": "otp",
"idempotency_key": idempotency_key
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
response = requests.post(
"https://api.smsroute.cc/v1/messages",
json=payload,
headers=headers
)
if response.status_code == 201:
result = response.json()
print(f"Message sent: {result['id']}")
return result
else:
print(f"Error: {response.status_code} - {response.text}")
return None
# Usage
send_otp("+33612345678", "123456")
Node.js (axios)
const axios = require('axios');
const crypto = require('crypto');
async function sendOTP(phoneNumber, code) {
const apiKey = 'sk_live_YOUR_API_KEY';
// Generate idempotency key to prevent duplicate sends on retry
const idempotencyKey = crypto
.createHash('sha256')
.update(`${phoneNumber}_${Date.now()}`)
.digest('hex');
const payload = {
to: phoneNumber,
body: `@example.com #${code} Your verification code. Valid 10 minutes.`,
traffic_class: 'otp',
idempotency_key: idempotencyKey
};
const config = {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
};
try {
const response = await axios.post(
'https://api.smsroute.cc/v1/messages',
payload,
config
);
console.log(`Message sent: ${response.data.id}`);
return response.data;
} catch (error) {
console.error(`Error: ${error.response.status} - ${error.response.data}`);
return null;
}
}
// Usage
sendOTP('+33612345678', '123456');
Complete validation flow (Python)
import time
from datetime import datetime, timedelta
class OTPValidator:
def __init__(self, expiry_seconds=300):
self.codes = {} # {user_id: {"code": "123456", "expiry": timestamp, "attempts": 0}}
self.expiry_seconds = expiry_seconds
self.max_attempts = 3
def generate_and_send(self, user_id, phone_number):
"""Generate OTP, send via smsroute, store server-side."""
import random
code = ''.join([str(random.randint(0, 9)) for _ in range(6)])
expiry = time.time() + self.expiry_seconds
self.codes[user_id] = {
"code": code,
"expiry": expiry,
"attempts": 0,
"phone": phone_number
}
# Call send_otp() from previous example
send_otp(phone_number, code)
return {"status": "sent"}
def validate(self, user_id, submitted_code):
"""Validate submitted code against stored OTP."""
if user_id not in self.codes:
return {"valid": False, "reason": "no_code_requested"}
record = self.codes[user_id]
# Check expiry
if time.time() > record["expiry"]:
del self.codes[user_id]
return {"valid": False, "reason": "expired"}
# Check attempts
if record["attempts"] >= self.max_attempts:
del self.codes[user_id]
return {"valid": False, "reason": "max_attempts"}
# Check code
if submitted_code != record["code"]:
record["attempts"] += 1
return {
"valid": False,
"reason": "invalid_code",
"attempts_remaining": self.max_attempts - record["attempts"]
}
# Success
del self.codes[user_id]
return {"valid": True}
# Usage
validator = OTPValidator(expiry_seconds=300)
validator.generate_and_send("user_123", "+33612345678")
result = validator.validate("user_123", "123456")
print(result) # {"valid": True}
Frequently asked questions
How do I generate a cryptographically secure OTP code?
Use your language's cryptographic random library:
- Python:
secrets.randbelow(1000000)for 6-digit code. - Node.js:
crypto.randomInt(1000000). - Java:
SecureRandom.nextInt(1000000).
Avoid Math.random() or rand()—they are not cryptographically secure.
Should I include the phone number in the OTP message?
No. This leaks information to attackers and clutters the message. The user already knows their phone number; they're reading the SMS on that phone. Instead, include the service name and action context: "Google: 123456 is your sign-in code."
Can I customize the message body for different locales?
Yes. Detect the user's language preference in your backend and compose the message accordingly. smsroute will auto-detect the script and route as GSM-7 or UCS-2. Example:
- English: "Your code: 123456. Valid 10 minutes." (GSM-7)
- Russian: "Ваш код: 123456. Действителен 10 минут." (UCS-2)
- Arabic: "الرمز الخاص بك: 123456. صالح 10 دقائق." (UCS-2)
What's the difference between traffic_class=otp and a regular SMS?
Technically, there's no difference in delivery. The traffic_class=otp flag helps smsroute optimize routing (OTP traffic has stricter latency requirements) and aids in analytics. It's optional but recommended for all OTP messages.
How do I handle OTP delivery failures?
Check the HTTP response code:
- 201 Created: Message queued for delivery. Check delivery status via webhooks or the message status endpoint.
- 400 Bad Request: Invalid phone number or payload. Fix and retry.
- 401 Unauthorized: Invalid API key. Check credentials.
- 429 Too Many Requests: Rate limit exceeded. Implement exponential backoff.
- 5xx Server Error: Temporary issue. Retry with exponential backoff (max 3 attempts).
If delivery fails repeatedly, show the user a fallback option (e.g., "We couldn't send SMS, try email instead").
Is smsroute GDPR compliant?
Yes. We comply with GDPR, CCPA, and regional data protection laws. Phone numbers are encrypted in transit and at rest. We don't retain OTP codes or user data beyond what's necessary for delivery. See our privacy policy for details.
Can I use smsroute for marketing SMS as well as OTP?
Yes. smsroute handles both OTP and bulk messaging. However, OTP and marketing have different compliance requirements (OTP doesn't require prior consent; marketing usually does). Segment your messages appropriately and use traffic_class=otp only for genuine one-time codes.
What SLA does smsroute offer?
99.9% uptime and 99% tier-1 delivery rates. Sub-second latency (p99 <500ms) across 149 countries. We route through the largest and most reliable carriers. See our status page (status.smsroute.cc) for real-time metrics.
Related pages
Explore more OTP and messaging topics:
- 2FA SMS delivery — Deep dive into two-factor authentication with SMS as the second factor.
- Python integration guide — Full walkthrough of integrating smsroute with Python backends.
- Node.js integration guide — Complete example for Express.js and other Node frameworks.
- Migrate from Twilio — Step-by-step guide to switching from Twilio to smsroute with zero downtime.
- API reference — Full endpoint documentation and authentication details.
- Pricing — Transparent pricing with no hidden fees. From $0.004/SMS across 149 countries.
Ready to send OTPs globally?
From $0.004/SMS across 149 countries. Crypto-only payments (BTC, USDT, ETH, LTC, XMR, SOL). No KYC at signup; $5 minimum top-up. Get started in minutes.
View API Documentation See Pricing
What's the difference between OTP and 2FA?
OTP (one-time password) is the broader category covering any single-use code: magic links, passwordless login, transaction verification, and account recovery. 2FA (two-factor authentication) is one specific OTP use case—a second factor after password entry. All 2FA is OTP; not all OTP is 2FA.
How long should an OTP be valid?
Transactional OTPs (e.g., confirm payment) should expire in 30–60 seconds. Recovery OTPs (e.g., password reset) may last 10 minutes. Balance security (shorter = safer) against UX (longer = more forgiveness).
Why does my message use UCS-2 encoding?
Non-Latin scripts (Cyrillic, Arabic, CJK, Devanagari) automatically trigger UCS-2 encoding. This reduces per-segment capacity from 160 characters (GSM-7) to 70 characters (UCS-2). smsroute detects script and encodes automatically.
How do I enable iOS autofill for OTP?
Prepend '@domain.com #' to your OTP message. For example: '@example.com #123456' triggers iOS Messages to auto-populate the code. The domain must match your app's verified domain.
What happens if a user clicks resend twice?
Use idempotency_key to deduplicate. If both requests have the same key, smsroute returns the same message ID and code. This prevents two different codes being active simultaneously.
Can smsroute protect against SS7 interception?
SMS is not end-to-end encrypted and vulnerable to SS7 attacks at the carrier level. For high-security scenarios, use push notifications as primary factor with SMS as fallback, or combine OTP with biometric verification.
How does sandbox mode work?
Use a test API key (sk_test_*) to send to test numbers without triggering real SMS. Responses are deterministic and instant—useful for CI/CD pipelines and integration testing.
What pricing should I expect?
From $0.004/SMS across 149 countries. OTP messages are standard SMS—no upcharge. Crypto-only payments (BTC, USDT, ETH, LTC, XMR, SOL) with no KYC required at signup. Minimum $5 top-up.
What's the difference between OTP and 2FA?
OTP (one-time password) is the broader category covering any single-use code: magic links, passwordless login, transaction verification, and account recovery. 2FA (two-factor authentication) is one specific OTP use case—a second factor after password entry. All 2FA is OTP; not all OTP is 2FA.
How long should an OTP be valid?
Transactional OTPs (e.g., confirm payment) should expire in 30–60 seconds. Recovery OTPs (e.g., password reset) may last 10 minutes. Balance security (shorter = safer) against UX (longer = more forgiveness).
Why does my message use UCS-2 encoding?
Non-Latin scripts (Cyrillic, Arabic, CJK, Devanagari) automatically trigger UCS-2 encoding. This reduces per-segment capacity from 160 characters (GSM-7) to 70 characters (UCS-2). smsroute detects script and encodes automatically.
How do I enable iOS autofill for OTP?
Prepend '@domain.com #' to your OTP message. For example: '@example.com #123456' triggers iOS Messages to auto-populate the code. The domain must match your app's verified domain.
What happens if a user clicks resend twice?
Use idempotency_key to deduplicate. If both requests have the same key, smsroute returns the same message ID and code. This prevents two different codes being active simultaneously.
Can smsroute protect against SS7 interception?
SMS is not end-to-end encrypted and vulnerable to SS7 attacks at the carrier level. For high-security scenarios, use push notifications as primary factor with SMS as fallback, or combine OTP with biometric verification.
How does sandbox mode work?
Use a test API key (sk_test_*) to send to test numbers without triggering real SMS. Responses are deterministic and instant—useful for CI/CD pipelines and integration testing.
What pricing should I expect?
From $0.004/SMS across 149 countries. OTP messages are standard SMS—no upcharge. Crypto-only payments (BTC, USDT, ETH, LTC, XMR, SOL) with no KYC required at signup. Minimum $5 top-up.
What's the difference between OTP and 2FA?
OTP (one-time password) is the broader category covering any single-use code: magic links, passwordless login, transaction verification, and account recovery. 2FA (two-factor authentication) is one specific OTP use case—a second factor after password entry. All 2FA is OTP; not all OTP is 2FA.
How long should an OTP be valid?
Transactional OTPs (e.g., confirm payment) should expire in 30–60 seconds. Recovery OTPs (e.g., password reset) may last 10 minutes. Balance security (shorter = safer) against UX (longer = more forgiveness).
Why does my message use UCS-2 encoding?
Non-Latin scripts (Cyrillic, Arabic, CJK, Devanagari) automatically trigger UCS-2 encoding. This reduces per-segment capacity from 160 characters (GSM-7) to 70 characters (UCS-2). smsroute detects script and encodes automatically.
How do I enable iOS autofill for OTP?
Prepend '@domain.com #' to your OTP message. For example: '@example.com #123456' triggers iOS Messages to auto-populate the code. The domain must match your app's verified domain.
What happens if a user clicks resend twice?
Use idempotency_key to deduplicate. If both requests have the same key, smsroute returns the same message ID and code. This prevents two different codes being active simultaneously.
Can smsroute protect against SS7 interception?
SMS is not end-to-end encrypted and vulnerable to SS7 attacks at the carrier level. For high-security scenarios, use push notifications as primary factor with SMS as fallback, or combine OTP with biometric verification.
How does sandbox mode work?
Use a test API key (sk_test_*) to send to test numbers without triggering real SMS. Responses are deterministic and instant—useful for CI/CD pipelines and integration testing.
What pricing should I expect?
From $0.004/SMS across 149 countries. OTP messages are standard SMS—no upcharge. Crypto-only payments (BTC, USDT, ETH, LTC, XMR, SOL) with no KYC required at signup. Minimum $5 top-up.
What's the difference between OTP and 2FA?
OTP (one-time password) is the broader category covering any single-use code: magic links, passwordless login, transaction verification, and account recovery. 2FA (two-factor authentication) is one specific OTP use case—a second factor after password entry. All 2FA is OTP; not all OTP is 2FA.
How long should an OTP be valid?
Transactional OTPs (e.g., confirm payment) should expire in 30–60 seconds. Recovery OTPs (e.g., password reset) may last 10 minutes. Balance security (shorter = safer) against UX (longer = more forgiveness).
Why does my message use UCS-2 encoding?
Non-Latin scripts (Cyrillic, Arabic, CJK, Devanagari) automatically trigger UCS-2 encoding. This reduces per-segment capacity from 160 characters (GSM-7) to 70 characters (UCS-2). smsroute detects script and encodes automatically.
How do I enable iOS autofill for OTP?
Prepend '@domain.com #' to your OTP message. For example: '@example.com #123456' triggers iOS Messages to auto-populate the code. The domain must match your app's verified domain.
What happens if a user clicks resend twice?
Use idempotency_key to deduplicate. If both requests have the same key, smsroute returns the same message ID and code. This prevents two different codes being active simultaneously.
Can smsroute protect against SS7 interception?
SMS is not end-to-end encrypted and vulnerable to SS7 attacks at the carrier level. For high-security scenarios, use push notifications as primary factor with SMS as fallback, or combine OTP with biometric verification.
How does sandbox mode work?
Use a test API key (sk_test_*) to send to test numbers without triggering real SMS. Responses are deterministic and instant—useful for CI/CD pipelines and integration testing.
What pricing should I expect?
From $0.004/SMS across 149 countries. OTP messages are standard SMS—no upcharge. Crypto-only payments (BTC, USDT, ETH, LTC, XMR, SOL) with no KYC required at signup. Minimum $5 top-up.