DIDfarm
  • Numbers
  • Trunks
  • Messaging
  • Connect
  • Pricing
  • Coverage
  • Help
🇬🇧 EN 🇳🇱 NL 🇩🇪 DE 🇫🇷 FR 🇪🇸 ES 🇧🇷 PT 🇸🇦 AR 🇨🇳 ZH 🇯🇵 JA 🇮🇳 HI
Sign in Get a Number
← Help Center
On this page
Overview Prerequisites How OTP Works Generate & Send OTP Verify the Code WhatsApp Fallback Branded Sender ID Rate Limiting Best Practices FAQ

OTP & 2FA via SMS

Messaging · 10 min read OTP SMS API

Overview

One-time passwords (OTP) sent via SMS are the most widely adopted method for two-factor authentication (2FA). Unlike email-based codes, SMS messages land on a device your user already has in their hand, delivering near-instant verification with open rates above 95%.

DIDfarm's SMS API lets you generate, deliver, and verify OTP codes in any of the 70+ countries where we provide numbers. This guide walks through the full integration, from generating a secure code to handling edge cases like expired tokens and delivery failures.

When to use SMS OTP: Account registration, login verification, password resets, payment confirmations, and any action where you need to prove the user controls a specific phone number.

Prerequisites

  • A DIDfarm account with at least one SMS-enabled number. You can purchase numbers at /buy.
  • An API key generated from the API Keys tab in your portal.
  • A server-side application capable of making HTTPS requests (Node.js, Python, PHP, etc.).
  • A database or in-memory store (Redis, Memcached) for persisting codes until verification.
Tip: Use a dedicated number for OTP traffic. This keeps your verification messages separate from marketing or transactional SMS and avoids carrier filtering issues.

How OTP Works

The OTP verification flow follows four steps:

Step Action Where
1 Generate a cryptographically random 6-digit code and store it with an expiry timestamp Your server
2 Send the code to the user's phone number via POST /api/v1/sms/messages DIDfarm API
3 User receives the SMS and enters the code into your application User's device
4 Your server compares the submitted code against the stored code and checks expiry Your server
Security note: Never send the OTP code back to the client in an API response. The code should only travel server → DIDfarm → SMS → user's phone → user input → server verification.

Generate & Send OTP

1

Generate a secure code

Use a cryptographically secure random number generator. Avoid Math.random() or similar non-secure functions.

Node.js
const crypto = require('crypto');

function generateOTP(length = 6) {
  const max = Math.pow(10, length);
  const code = crypto.randomInt(0, max);
  return code.toString().padStart(length, '0');
}

// Generate and store
const otp = generateOTP();
const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes

// Store in Redis (recommended) or your database
await redis.setex(`otp:${phoneNumber}`, 300, JSON.stringify({
  code: otp,
  attempts: 0,
  createdAt: Date.now()
}));
Python
import secrets
import time
import redis

def generate_otp(length=6):
    return ''.join([str(secrets.randbelow(10)) for _ in range(length)])

otp = generate_otp()
expires_at = int(time.time()) + 300  # 5 minutes

# Store in Redis
r = redis.Redis()
r.setex(
    f"otp:{phone_number}",
    300,
    json.dumps({"code": otp, "attempts": 0, "created_at": int(time.time())})
)
2

Send the OTP via DIDfarm API

Call the POST /api/v1/sms/messages endpoint with your OTP code embedded in the message body.

Node.js — Send OTP
const axios = require('axios');

async function sendOTP(to, code) {
  const response = await axios.post(
    'https://didfarm.com/api/v1/sms/messages',
    {
      from: '+31201234567',       // Your DIDfarm SMS-enabled number
      to: to,                     // Recipient in E.164 format
      body: `Your verification code is ${code}. It expires in 5 minutes.`,
      webhook_url: 'https://yourapp.com/webhooks/sms-status'
    },
    {
      headers: {
        'Authorization': `Bearer ${process.env.DIDFARM_API_KEY}`,
        'Content-Type': 'application/json'
      }
    }
  );

  return response.data;
}

// Usage
const result = await sendOTP('+4915112345678', otp);
console.log('Message ID:', result.data.message_id);
Python — Send OTP
import requests
import os

def send_otp(to: str, code: str) -> dict:
    response = requests.post(
        "https://didfarm.com/api/v1/sms/messages",
        json={
            "from": "+31201234567",
            "to": to,
            "body": f"Your verification code is {code}. It expires in 5 minutes.",
            "webhook_url": "https://yourapp.com/webhooks/sms-status"
        },
        headers={
            "Authorization": f"Bearer {os.environ['DIDFARM_API_KEY']}",
            "Content-Type": "application/json"
        }
    )
    response.raise_for_status()
    return response.json()

# Usage
result = send_otp("+4915112345678", otp)
print("Message ID:", result["data"]["message_id"])

API Response

Response — 201 Created
{
  "data": {
    "message_id": "msg_8f3a2b1c4d5e6f7a",
    "from": "+31201234567",
    "to": "+4915112345678",
    "status": "queued",
    "segments": 1,
    "created_at": "2026-04-05T10:30:00Z"
  }
}

Verify the Code

3

Compare the user's input against the stored code

When the user submits their code, retrieve the stored record, check expiry, and compare using a timing-safe comparison function to prevent timing attacks.

Node.js — Verify
const crypto = require('crypto');

async function verifyOTP(phoneNumber, submittedCode) {
  const record = await redis.get(`otp:${phoneNumber}`);
  if (!record) {
    return { valid: false, error: 'Code expired or not found' };
  }

  const data = JSON.parse(record);

  // Check max attempts (prevent brute force)
  if (data.attempts >= 5) {
    await redis.del(`otp:${phoneNumber}`);
    return { valid: false, error: 'Too many attempts. Request a new code.' };
  }

  // Increment attempt counter
  data.attempts += 1;
  await redis.setex(`otp:${phoneNumber}`, 300, JSON.stringify(data));

  // Timing-safe comparison
  const valid = crypto.timingSafeEqual(
    Buffer.from(submittedCode),
    Buffer.from(data.code)
  );

  if (valid) {
    await redis.del(`otp:${phoneNumber}`); // One-time use
    return { valid: true };
  }

  return { valid: false, error: 'Invalid code' };
}
Python — Verify
import hmac

def verify_otp(phone_number: str, submitted_code: str) -> dict:
    record = r.get(f"otp:{phone_number}")
    if not record:
        return {"valid": False, "error": "Code expired or not found"}

    data = json.loads(record)

    if data["attempts"] >= 5:
        r.delete(f"otp:{phone_number}")
        return {"valid": False, "error": "Too many attempts. Request a new code."}

    data["attempts"] += 1
    r.setex(f"otp:{phone_number}", 300, json.dumps(data))

    # Timing-safe comparison
    if hmac.compare_digest(submitted_code, data["code"]):
        r.delete(f"otp:{phone_number}")
        return {"valid": True}

    return {"valid": False, "error": "Invalid code"}

Delivery Status Webhook

DIDfarm sends delivery status updates to the webhook_url you specified when sending the message. Use this to detect failed deliveries and trigger a fallback (e.g., voice call or WhatsApp).

Webhook Payload — Delivered
{
  "event": "message.status_update",
  "data": {
    "message_id": "msg_8f3a2b1c4d5e6f7a",
    "to": "+4915112345678",
    "status": "delivered",
    "delivered_at": "2026-04-05T10:30:03Z",
    "error_code": null
  }
}
Webhook Payload — Failed
{
  "event": "message.status_update",
  "data": {
    "message_id": "msg_8f3a2b1c4d5e6f7a",
    "to": "+4915112345678",
    "status": "failed",
    "error_code": "30003",
    "error_message": "Unreachable destination handset"
  }
}

WhatsApp Fallback

For users on WhatsApp, you can send the OTP through WhatsApp first and fall back to SMS if the message is not delivered within 10 seconds. This reduces SMS costs and improves the user experience in markets where WhatsApp dominates (Western Europe, Latin America, India).

Node.js — WhatsApp with SMS Fallback
async function sendOTPWithFallback(to, code) {
  // Try WhatsApp first
  const whatsapp = await axios.post(
    'https://didfarm.com/api/v1/whatsapp/messages',
    {
      from: '+31201234567',
      to: to,
      template: 'otp_verification',
      variables: { code: code, expiry_minutes: '5' }
    },
    { headers: { 'Authorization': `Bearer ${process.env.DIDFARM_API_KEY}` } }
  );

  // Wait 10 seconds for delivery confirmation
  const delivered = await waitForDelivery(whatsapp.data.data.message_id, 10000);

  if (!delivered) {
    // Fall back to SMS
    console.log('WhatsApp not delivered, falling back to SMS');
    return sendOTP(to, code);
  }

  return whatsapp.data;
}
Tip: To use WhatsApp OTP templates, you must first register and get approval for your template through the DIDfarm WhatsApp Business dashboard. Templates typically take 24–48 hours for approval.

Branded Sender ID

In many European countries, you can replace the sending phone number with an alphanumeric sender ID (up to 11 characters). This displays your brand name instead of a phone number, increasing trust and open rates.

Send with Branded Sender ID
await axios.post('https://didfarm.com/api/v1/sms/messages', {
  from: 'YourBrand',              // Alphanumeric sender ID (max 11 chars)
  to: '+4915112345678',
  body: 'Your verification code is 847293. It expires in 5 minutes.',
  webhook_url: 'https://yourapp.com/webhooks/sms-status'
}, {
  headers: { 'Authorization': `Bearer ${process.env.DIDFARM_API_KEY}` }
});
Country Sender ID Support Notes
Netherlands Supported No pre-registration required
Germany Supported No pre-registration required
United Kingdom Supported No pre-registration required
France Restricted Must use a numeric sender for transactional SMS
Belgium Supported Pre-registration recommended
United States Not supported Use a local or toll-free number instead
Important: Users cannot reply to alphanumeric sender IDs. If you need two-way communication, use a standard phone number as the sender.

Rate Limiting

To prevent abuse and protect your account from fraudulent OTP pumping, enforce rate limits on both your application side and rely on DIDfarm's built-in protections.

Limit Value Scope
Minimum interval between OTPs 60 seconds Per recipient number
Maximum OTPs per hour 5 Per recipient number
Maximum OTPs per day 20 Per recipient number
Maximum verification attempts 5 Per code
Node.js — Rate Limit Check
async function checkRateLimit(phoneNumber) {
  const hourKey = `otp_rate:${phoneNumber}:hour`;
  const cooldownKey = `otp_rate:${phoneNumber}:cooldown`;

  // Check 60-second cooldown
  if (await redis.exists(cooldownKey)) {
    const ttl = await redis.ttl(cooldownKey);
    throw new Error(`Please wait ${ttl} seconds before requesting a new code.`);
  }

  // Check hourly limit
  const hourCount = parseInt(await redis.get(hourKey) || '0');
  if (hourCount >= 5) {
    throw new Error('Maximum OTP requests reached. Try again in one hour.');
  }

  // Set cooldown and increment counter
  await redis.setex(cooldownKey, 60, '1');
  await redis.incr(hourKey);
  if (hourCount === 0) {
    await redis.expire(hourKey, 3600);
  }
}

Best Practices

Code Generation

  • Use 6-digit numeric codes. They balance security with usability. Four digits are too easy to guess; eight digits frustrate users.
  • Always use a cryptographically secure random number generator (crypto.randomInt in Node.js, secrets in Python).
  • Never reuse a code. Each OTP should be single-use and deleted immediately after successful verification.

Expiry & Storage

  • Set a 5-minute expiry. This is the industry standard, balancing security with the time users need to switch between apps.
  • Store codes in Redis or Memcached with automatic TTL expiry rather than your primary database.
  • Never log OTP codes in plaintext. If you must log for debugging, hash them first.

User Experience

  • Include the expiry duration in the SMS body: "expires in 5 minutes".
  • Show a resend button in your UI with a 60-second countdown timer.
  • Auto-focus the OTP input field and use autocomplete="one-time-code" for automatic SMS autofill on mobile browsers.
  • Display the last 4 digits of the phone number so users know where to look: "Code sent to ***5678".

Security

  • Use timing-safe comparison (crypto.timingSafeEqual or hmac.compare_digest) to prevent timing attacks.
  • Limit verification attempts to 5 per code. After 5 failures, invalidate the code and require a new one.
  • Log all OTP requests and verification attempts for fraud detection.
  • Monitor for OTP pumping (automated requests to premium-rate numbers). Set up alerts for unusual spikes in OTP volume.
Production checklist: Cryptographic RNG, 5-min expiry, single-use codes, timing-safe comparison, rate limiting, delivery webhook monitoring, and audit logging.

FAQ

What if the SMS is not delivered?

Listen for the message.status_update webhook. If the status is failed or you do not receive a delivered status within 30 seconds, offer the user an alternative: resend the SMS, try WhatsApp, or use a voice call OTP.

Can I use the same number for OTP and marketing SMS?

Technically yes, but we recommend using separate numbers for OTP and marketing. Carrier spam filters are more aggressive with numbers that send mixed-content traffic, which can delay time-sensitive OTP deliveries.

How do I handle international numbers?

Always use E.164 format (e.g., +4915112345678) when sending to international numbers. DIDfarm automatically routes through the best carrier path for each destination country. For best delivery rates, use a sender number local to the recipient's country.

Is there a test/sandbox mode?

Yes. Use the X-DIDfarm-Test: true header in your API requests. Test messages are not delivered to handsets but return realistic API responses and webhook events, letting you validate your integration without incurring costs.

Ready to integrate OTP?

See the full API reference with all parameters, error codes, and webhooks.

View SMS API Reference →
© 2026 DIDfarm · didfarm.com
About Blog Partners Coverage API Docs Status Privacy Terms Cookies Help

We use essential cookies to make DIDfarm work. With your consent, we also use analytics cookies to improve our service. Cookie Policy