Skip to main content
This guide goes deep on the operational side of protecting your Telnyx Verify integration — server-side rate limiting architectures, geo-fencing, anomaly detection, cost controls, and incident response. For foundational security concepts, see the Security Best Practices guide.

Architecture overview

A robust fraud prevention system layers multiple defenses:
User Request → CAPTCHA → IP Rate Limit → Phone Rate Limit → Geo-fence → Anomaly Check → Telnyx Verify API
Each layer catches different attack patterns. No single defense is sufficient on its own.

Server-side rate limiting with Redis

Production rate limiting requires a distributed store. These examples use Redis for shared state across multiple application instances.

Sliding window rate limiter

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

/**
 * Sliding window rate limiter using Redis sorted sets.
 * @param {string} key - Rate limit key (e.g., phone number or IP)
 * @param {number} maxRequests - Maximum requests allowed
 * @param {number} windowMs - Time window in milliseconds
 * @returns {Promise<{allowed: boolean, remaining: number, retryAfterMs: number}>}
 */
async function checkRateLimit(key, maxRequests, windowMs) {
  const now = Date.now();
  const windowStart = now - windowMs;
  const redisKey = `ratelimit:${key}`;

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(redisKey, 0, windowStart);  // Remove expired
  pipeline.zcard(redisKey);                              // Count current
  pipeline.zadd(redisKey, now, `${now}-${Math.random()}`); // Add this request
  pipeline.expire(redisKey, Math.ceil(windowMs / 1000)); // Set TTL

  const results = await pipeline.exec();
  const currentCount = results[1][1];

  if (currentCount >= maxRequests) {
    // Remove the entry we just added
    await redis.zremrangebyscore(redisKey, now, now);
    const oldestEntry = await redis.zrange(redisKey, 0, 0, 'WITHSCORES');
    const retryAfterMs = oldestEntry.length > 1
      ? windowMs - (now - Number(oldestEntry[1]))
      : windowMs;

    return { allowed: false, remaining: 0, retryAfterMs };
  }

  return { allowed: true, remaining: maxRequests - currentCount - 1, retryAfterMs: 0 };
}

// Usage: Multi-layer rate limiting
async function handleVerificationRequest(req) {
  const phone = req.body.phone_number;
  const ip = req.ip;
  const userId = req.user?.id;

  // Layer 1: IP rate limit (10/hour)
  const ipCheck = await checkRateLimit(`ip:${ip}`, 10, 3600000);
  if (!ipCheck.allowed) {
    return { status: 429, retryAfter: ipCheck.retryAfterMs };
  }

  // Layer 2: Phone rate limit (3/10min)
  const phoneCheck = await checkRateLimit(`phone:${phone}`, 3, 600000);
  if (!phoneCheck.allowed) {
    return { status: 429, retryAfter: phoneCheck.retryAfterMs };
  }

  // Layer 3: User rate limit (5/hour)
  if (userId) {
    const userCheck = await checkRateLimit(`user:${userId}`, 5, 3600000);
    if (!userCheck.allowed) {
      return { status: 429, retryAfter: userCheck.retryAfterMs };
    }
  }

  // All checks passed — send verification
  return await sendVerification(phone);
}

Geo-fencing

Restrict verifications to countries where your service operates. This is the single most effective defense against SMS pumping.

Configure on 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", "GB", "AU"]
    }
  }'

Application-level geo-validation

Add server-side validation before calling the API as a defense-in-depth measure:
import { parsePhoneNumber } from 'libphonenumber-js';

const ALLOWED_COUNTRIES = new Set(['US', 'CA', 'GB', 'AU']);

function validatePhoneCountry(phoneNumber) {
  const parsed = parsePhoneNumber(phoneNumber);
  if (!parsed || !parsed.country) {
    throw new Error('Invalid phone number');
  }
  if (!ALLOWED_COUNTRIES.has(parsed.country)) {
    throw new Error('Verification not available in this region');
  }
  return parsed.country;
}

High-risk country codes

These country codes are frequently targeted for SMS pumping and toll fraud. Block or add extra scrutiny:
CodeCountryRisk
+232Sierra LeoneSMS pumping
+225Côte d’IvoireSMS pumping
+233GhanaSMS pumping
+234NigeriaMixed (legitimate + fraud)
+260ZambiaSMS pumping
+256UgandaSMS pumping
+880BangladeshToll fraud
+855CambodiaToll fraud
+856LaosToll fraud
+960MaldivesToll fraud
+592GuyanaToll fraud
Note: These are statistical patterns, not blanket rules. If you serve users in these countries, implement stronger rate limiting rather than blocking.

Anomaly detection

Build automated detection for suspicious patterns beyond simple rate limits.

Conversion rate monitoring

A healthy verification flow has a 60-80% conversion rate (codes sent vs. codes verified). A rate below 20% may indicate an attack.
class ConversionMonitor {
  constructor(redis, alertCallback) {
    this.redis = redis;
    this.alertCallback = alertCallback;
  }

  async trackSent(phoneNumber) {
    const hour = Math.floor(Date.now() / 3600000);
    await this.redis.incr(`verify:sent:${hour}`);
    await this.redis.expire(`verify:sent:${hour}`, 7200);
  }

  async trackVerified(phoneNumber) {
    const hour = Math.floor(Date.now() / 3600000);
    await this.redis.incr(`verify:verified:${hour}`);
    await this.redis.expire(`verify:verified:${hour}`, 7200);
  }

  async checkConversionRate() {
    const hour = Math.floor(Date.now() / 3600000);
    const sent = parseInt(await this.redis.get(`verify:sent:${hour}`)) || 0;
    const verified = parseInt(await this.redis.get(`verify:verified:${hour}`)) || 0;

    if (sent < 10) return; // Too few samples

    const rate = verified / sent;
    if (rate < 0.2) {
      this.alertCallback({
        message: `Low verification conversion rate: ${(rate * 100).toFixed(1)}%`,
        sent,
        verified,
        hour: new Date(hour * 3600000).toISOString(),
      });
    }
  }
}

Sequential number detection

SMS pumping often uses sequential phone numbers. Detect and block this pattern:
function detectSequentialNumbers(recentNumbers, threshold = 5) {
  if (recentNumbers.length < threshold) return false;

  // Sort by numeric value
  const sorted = recentNumbers
    .map(n => BigInt(n.replace(/\D/g, '')))
    .sort((a, b) => (a < b ? -1 : 1));

  // Check for sequences
  let sequential = 1;
  for (let i = 1; i < sorted.length; i++) {
    if (sorted[i] - sorted[i - 1] <= 3n) {
      sequential++;
      if (sequential >= threshold) return true;
    } else {
      sequential = 1;
    }
  }

  return false;
}

Cost controls

Set spend alerts

Monitor your Telnyx account spending and set alerts at the account level through the Telnyx Portal billing settings.

Implement circuit breakers

Automatically disable verifications when anomalies are detected:
class VerificationCircuitBreaker {
  constructor(redis, maxPerHour = 500) {
    this.redis = redis;
    this.maxPerHour = maxPerHour;
    this.tripped = false;
  }

  async canSend() {
    if (this.tripped) return false;

    const hour = Math.floor(Date.now() / 3600000);
    const count = parseInt(await this.redis.get(`verify:total:${hour}`)) || 0;

    if (count >= this.maxPerHour) {
      this.tripped = true;
      // Alert operations team
      console.error(`Circuit breaker tripped: ${count} verifications in current hour`);
      return false;
    }

    await this.redis.incr(`verify:total:${hour}`);
    await this.redis.expire(`verify:total:${hour}`, 7200);
    return true;
  }

  reset() {
    this.tripped = false;
  }
}

Incident response

When you detect a fraud attack in progress:
1

Immediately: Enable circuit breaker

Stop all verification sends to limit financial damage.
2

Investigate: Check patterns

Look at the destination countries, IP addresses, and phone number patterns in your logs.
3

Block: Update allowlists

Remove affected countries from your Verify profile’s whitelisted_destinations.
4

Recover: Tighten limits

Reduce rate limits, add CAPTCHA if not present, and re-enable verifications gradually.
5

Contact Telnyx Support

Report the incident to Telnyx Support for investigation and potential charge reversal.

Configuration reference

Summary of all Verify profile settings relevant to fraud prevention:
SettingEndpointPurpose
whitelisted_destinationsPATCH /v2/verify_profiles/{id}Restrict SMS to specific countries
code_lengthPATCH /v2/verify_profiles/{id}Set verification code length (4-10)
default_timeout_secsPATCH /v2/verify_profiles/{id}Expiration time for codes
# Example: Production-hardened 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
    }
  }'

Next steps