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:

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:

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:

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.

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:

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:

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:

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:

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.