Skip to main content
Verification flows are a high-value target for attackers. SMS pumping, toll fraud, brute-force code guessing, and social engineering can cost you money and compromise user accounts. This guide covers practical defenses you should implement alongside Telnyx Verify.

Threat overview

ThreatDescriptionImpact
SMS pumpingAttackers trigger thousands of SMS verifications to premium-rate numbersInflated costs, sometimes $10K+ per incident
Toll fraud (IRSF)Exploiting call verification to generate revenue on premium international numbersPer-minute charges on fraudulent calls
Brute-force attacksSystematically guessing verification codesAccount takeover
Code interceptionSIM swapping, SS7 attacks, malware intercepting SMSAccount compromise
EnumerationUsing verification endpoints to check if phone numbers exist in your systemPrivacy leak, targeted attacks

Rate limiting

Rate limiting is your first line of defense against abuse. Apply limits at multiple layers.

Per-phone-number limits

Restrict how many verification attempts a single phone number can trigger within a time window.
import { RateLimiterMemory } from 'rate-limiter-flexible';

// Max 3 verification requests per phone number per 10 minutes
const phoneLimiter = new RateLimiterMemory({
  points: 3,
  duration: 600, // 10 minutes
});

async function requestVerification(phoneNumber) {
  try {
    await phoneLimiter.consume(phoneNumber);
    // Proceed with Telnyx Verify API call
  } catch (rejRes) {
    const retryAfter = Math.ceil(rejRes.msBeforeNext / 1000);
    throw new Error(`Too many attempts. Try again in ${retryAfter} seconds.`);
  }
}

Per-IP address limits

Prevent a single IP from triggering verifications for many different numbers (a hallmark of SMS pumping):
// Max 10 verification requests per IP per hour
const ipLimiter = new RateLimiterMemory({
  points: 10,
  duration: 3600,
});

app.post('/verify/request', async (req, res) => {
  const clientIp = req.ip;
  try {
    await ipLimiter.consume(clientIp);
    await phoneLimiter.consume(req.body.phone_number);
    // Proceed with verification
  } catch {
    res.status(429).json({ error: 'Too many requests' });
  }
});
ScopeLimitWindow
Per phone number3 attempts10 minutes
Per phone number5 attempts1 hour
Per IP address10 attempts1 hour
Per account/session5 attempts1 hour
Global (all numbers)Monitor for spikesContinuous

SMS pumping prevention

SMS pumping is the most costly fraud vector for verification flows. Attackers abuse your send endpoint to generate SMS revenue on number ranges they control.

Detection signals

Watch for these patterns:
  • Sequential numbers — Verification requests for +1234500001, +1234500002, +1234500003
  • Unusual country codes — Spike in verifications to countries you don’t serve
  • High failure rate — Many verifications triggered but never completed
  • Burst traffic — Sudden spike in verification requests from a single source

Defenses

1

Restrict destination countries

Configure whitelisted_destinations on your Verify profile to only allow countries where your users are:
curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sms": {
      "whitelisted_destinations": ["US", "CA", "GB"]
    }
  }'
2

Require authentication before verification

Don’t expose your verification endpoint to unauthenticated users. Require at least a session or account to trigger a verification.
3

Add CAPTCHA

Place a CAPTCHA (reCAPTCHA, hCaptcha, Turnstile) before the phone number input to block automated submissions.
4

Monitor and alert

Set up alerts for unusual verification volume:
// Track verification requests per minute
const verifyCount = new Map();

function trackVerification() {
  const minute = Math.floor(Date.now() / 60000);
  verifyCount.set(minute, (verifyCount.get(minute) || 0) + 1);

  if (verifyCount.get(minute) > 100) {
    // Alert: possible SMS pumping attack
    alertOps('Verification spike detected: ' + verifyCount.get(minute) + '/min');
  }
}

Code security

Use appropriate code length

Longer codes are harder to brute-force but harder for users to enter. Balance security and usability:
Code LengthCombinationsBrute-force time (3 attempts/min)Recommendation
4 digits10,000~55 hoursLow security only
5 digits100,000~23 daysDefault — good balance
6 digits1,000,000~231 daysHigh security applications
Configure code length in your Verify profile:
curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sms": {
      "code_length": 6
    }
  }'

Set appropriate timeouts

Short timeouts reduce the window for brute-force attacks:
curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sms": {
      "default_timeout_secs": 300
    },
    "call": {
      "default_timeout_secs": 300
    }
  }'
A 5-minute timeout (300 seconds) works well for most applications. Shorter timeouts (120s) add security but may frustrate users on slow networks.

Limit verification attempts

Lock out after too many failed code entries to prevent brute-force:
const failedAttempts = new Map();

async function verifyCode(phoneNumber, code) {
  const attempts = failedAttempts.get(phoneNumber) || 0;

  if (attempts >= 5) {
    throw new Error('Too many failed attempts. Request a new code.');
  }

  const response = await fetch(
    `https://api.telnyx.com/v2/verifications/by_phone_number/${phoneNumber}/actions/verify`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.TELNYX_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ code, verify_profile_id: PROFILE_ID }),
    }
  );
  const result = await response.json();

  if (result.data.response_code === 'accepted') {
    failedAttempts.delete(phoneNumber);
    return true;
  }

  failedAttempts.set(phoneNumber, attempts + 1);
  return false;
}

Prevent number enumeration

Don’t reveal whether a phone number exists in your system through verification responses:
❌ Vulnerable — reveals whether the number is registered:
{ "error": "No account found for this phone number" }
✅ Secure — same response regardless:
{ "message": "If this number is registered, you'll receive a verification code." }
Always return a consistent response and send the verification (or silently drop it) regardless of whether the number exists in your system.

Channel fallback strategy

Use multiple verification channels to improve delivery and security:
1

Primary: SMS

Start with SMS verification — widest reach and fastest delivery.
2

Fallback: Voice call

If SMS isn’t delivered within 30 seconds, offer a voice call option. This helps users on networks with delayed SMS delivery.
3

Consider: Flashcall

For supported markets, flashcall verification (where the phone number itself is the code) provides instant verification with no user input required.
Configure all three channels on your Verify profile:
curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sms": {
      "whitelisted_destinations": ["US", "CA"],
      "default_timeout_secs": 300,
      "code_length": 6
    },
    "call": {
      "default_timeout_secs": 300
    },
    "flashcall": {
      "default_timeout_secs": 300
    }
  }'

Webhook security for Verify

Secure your verification webhook endpoint to prevent spoofed delivery notifications:
  1. Allowlist Telnyx IPs — Only accept webhooks from 192.76.120.192/27
  2. Use HTTPS — Never use plain HTTP for webhook endpoints
  3. Validate payload structure — Check for expected fields before processing
  4. Don’t trust client-side status — Always verify through webhooks or API, never trust client-reported verification status
import { createServer } from 'http';
import { networkInterfaces } from 'os';

const TELNYX_WEBHOOK_CIDR = '192.76.120.192/27';

function isFromTelnyx(ip) {
  // In production, use a proper CIDR matching library
  const parts = ip.split('.').map(Number);
  return parts[0] === 192 && parts[1] === 76 &&
         parts[2] === 120 && parts[3] >= 192 && parts[3] <= 223;
}

app.post('/webhooks/verify', (req, res) => {
  const clientIp = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;

  if (!isFromTelnyx(clientIp)) {
    console.warn(`Rejected webhook from unauthorized IP: ${clientIp}`);
    return res.sendStatus(403);
  }

  // Process webhook
  const event = req.body.data;
  if (event.event_type === 'verify.delivered') {
    console.log(`Verification delivered to ${event.payload.phone_number}`);
  }

  res.sendStatus(200);
});

Security checklist

Use this checklist when implementing Telnyx Verify in production:
  • Per-phone-number rate limit (3/10min)
  • Per-IP rate limit (10/hour)
  • Per-account/session rate limit
  • Global volume monitoring and alerting
  • Country allowlist configured on Verify profile
  • CAPTCHA before verification trigger
  • Authentication required before sending verification
  • SMS pumping detection (sequential numbers, country spikes)
  • Appropriate code length (5-6 digits)
  • Short timeout (300 seconds or less)
  • Max failed attempts lockout (5 attempts)
  • Consistent responses (no number enumeration)
  • HTTPS webhook endpoints
  • Telnyx IP allowlisting for webhooks
  • Server-side verification only (never trust client)
  • Logging and monitoring for anomalies

Next steps