Guides

OTP Bombing Attacks in India: How to Protect Your API Before It Costs You ₹1 Lakh a Day

Learn how otp bombing attacks drain Indian developer budgets. A technical guide on Redis rate limiting, device fingerprinting, CAPTCHA gating, and phone validation to protect your OTP endpoint.

StartMessaging Team
OTP Bombing Attacks in India: How to Protect Your API Before It Costs You ₹1 Lakh a Day

Ananya’s phone buzzed 47 times in 90 seconds. Each notification was an OTP — from food delivery apps, payment wallets, e-commerce platforms, and ride-hailing services she had accounts with. None of them were initiated by her. Three hundred kilometres away, a developer named Rohan stared at his startup’s SMS dashboard. His OTP send volume had spiked from the usual 800 per hour to 38,000 in the past 45 minutes. His wallet balance, prepaid at ₹0.25 per OTP, had dropped by ₹9,500 before he noticed. Ananya was the harassment victim. Rohan was the financial victim. Both were targets of OTP bombing — an attack that is now industrialised, automated, and specifically targeting Indian API endpoints. In February 2026, Cyble Research and Intelligence Labs identified India as one of the primary regional concentrations of OTP bombing campaigns, cataloguing approximately 843 vulnerable API endpoints exposed to these tools. This guide shows you what OTP bombing actually is, how attackers find your endpoint, the real cost math, and a four-layer defence stack with production-ready Node.js code.


What OTP Bombing Actually Is — And Why Developers Get It Wrong

Most developers conflate two different attacks under the label “OTP bombing.” The distinction matters because the defences differ.

SMS flooding (user harassment): An attacker targets a single phone number by triggering OTP sends from dozens of different applications simultaneously. The victim’s phone is flooded with messages. The attacker’s goal is harassment or distraction — sometimes used as a smokescreen while executing account takeover on one of those services. The attacker does not need access to your specific API; they use publicly available OTP bombing tools that catalogue hundreds of unprotected endpoints across Indian apps.

API abuse (endpoint exploitation): An attacker targets your specific /send-otp endpoint, hitting it with thousands of requests using different phone numbers. The goal is to drain your SMS wallet, crash your OTP delivery pipeline, or — in revenue-share arrangements — profit from termination fees. This is the attack that costs you ₹1 lakh a day.

SMS Flooding (Harassment)            API Abuse (Exploitation)
┌──────────────────────────┐         ┌──────────────────────────┐
│ Attacker targets ONE     │         │ Attacker targets YOUR    │
│ victim's phone number    │         │ send-otp endpoint        │
│                          │         │                          │
│ Triggers OTPs from       │         │ Sends thousands of       │
│ 50+ different apps       │         │ requests with different  │
│                          │         │ phone numbers            │
│ Goal: Harass the victim  │         │ Goal: Drain your wallet  │
│ Cost to you: 1 OTP       │         │ Cost to you: ₹25,000/hr+ │
└──────────────────────────┘         └──────────────────────────┘

Both attacks exploit the same vulnerability: an unprotected OTP send endpoint. The Cyble February 2026 report found that the tools used for these attacks are no longer simple Python scripts. They are cross-platform desktop applications with GUI interfaces and Telegram-integrated bots that allow attackers to orchestrate campaigns with a few clicks. Some tools support configurable delays, proxy rotation, and target list imports — essentially industrialised OTP abuse.


How Attackers Find Your Endpoint

The reconnaissance phase is embarrassingly simple. Here is what attackers look for and what a vulnerable Express.js handler looks like:

The Discovery Checklist Attackers Use

  1. Unauthenticated routes: The /send-otp or /api/auth/otp endpoint accepts a phone number and sends an OTP with no prior authentication, session token, or device binding.
  2. No CAPTCHA: The endpoint has no challenge-response mechanism. A bot can POST directly without solving anything.
  3. No IP throttling: The endpoint returns 200 OK to 1,000 requests from the same IP in the same minute.
  4. Predictable error messages: The endpoint returns different responses for “number already registered” versus “new number,” allowing attackers to enumerate valid accounts.
  5. Client-side-only validation: Phone number format checks exist only in the frontend; the backend accepts anything.

A Vulnerable Handler (Do Not Ship This)

// ❌ VULNERABLE — this is what attackers look for
app.post('/api/send-otp', async (req, res) => {
  const { phone } = req.body;

  // No rate limiting
  // No CAPTCHA verification
  // No IP check
  // No phone number validation

  const otp = Math.floor(100000 + Math.random() * 900000);
  await smsProvider.send(phone, `Your OTP is ${otp}`);
  await db.otps.create({ phone, otp, expiresAt: Date.now() + 300000 });

  res.json({ success: true, message: 'OTP sent' });
});

This handler is the default pattern in dozens of Indian startup codebases. It accepts any phone number, sends an OTP immediately, and returns a success response. No friction, no cost control, no abuse prevention. An attacker script can call this endpoint 10,000 times per hour with rotating phone numbers and drain your entire prepaid SMS wallet overnight.


The Cost Math: Why This Is a ₹1 Lakh Problem

At the OTP rates most Indian developers pay, the financial damage accumulates fast. Here is the arithmetic using real attack scales reported by Cyble and observed by Indian SMS providers:

Attack IntensityRequests/HourCost per OTPHourly CostDaily Cost (24 hrs)
Low (single bot)5,000₹0.25₹1,250₹30,000
Medium (distributed)50,000₹0.25₹12,500₹3,00,000
High (coordinated)1,00,000₹0.25₹25,000₹6,00,000
Weekend unattended4,00,000+₹0.25₹1,00,000₹24,00,000

The “weekend unattended” scenario is the nightmare case — and it is real. SMS pumping fraud attacks commonly target Saturday nights and public holidays when engineering teams are offline. A startup with a ₹50,000 prepaid SMS wallet can have it emptied before anyone checks the dashboard on Monday morning.

Beyond the direct SMS cost, there are secondary damages:

  • DLT scrubbing fees: Every OTP sent through a DLT-registered route incurs telecom operator fees on top of the SMS delivery charge. You pay twice for fraudulent messages.
  • Delivery pipeline degradation: A flood of bot-triggered OTPs can saturate your SMS provider’s queue, delaying delivery to legitimate users and tanking your OTP delivery rates.
  • User trust damage: If a user’s phone is flooded with your OTPs as part of a harassment campaign, they associate your brand with spam.

Layer 1: Phone Number Rate Limiting with Redis

This is the minimum viable defence. Without per-number rate limiting, your endpoint is an open wallet. The implementation uses a Redis sliding window to enforce a maximum of 3 OTP sends per phone number per hour.

Sliding Window vs Fixed Window

A fixed window resets at the top of each hour. An attacker can send 3 OTPs at 10:59 and 3 more at 11:01, getting 6 sends in 2 minutes. A sliding window tracks the actual timestamps of recent sends, so the limit is always enforced over the trailing 60-minute period. Use sliding windows for OTP rate limiting — the marginal Redis cost is negligible and the protection is meaningfully better.

// otp-rate-limiter.js — Sliding window rate limiting with Redis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

const MAX_OTP_PER_NUMBER_PER_HOUR = 3;
const WINDOW_MS = 3600000; // 1 hour
const MIN_INTERVAL_MS = 60000; // 60 seconds between sends

async function checkPhoneRateLimit(phoneNumber) {
  const key = `otp:ratelimit:phone:${phoneNumber}`;
  const now = Date.now();

  // Remove entries outside the sliding window
  await redis.zremrangebyscore(key, 0, now - WINDOW_MS);

  // Check how many sends are in the current window
  const count = await redis.zcard(key);

  if (count >= MAX_OTP_PER_NUMBER_PER_HOUR) {
    return {
      allowed: false,
      reason: 'Maximum OTP requests reached for this number. Try again later.',
      retryAfter: Math.ceil(WINDOW_MS / 1000),
    };
  }

  // Check minimum interval between sends
  const lastSend = await redis.zrange(key, -1, -1, 'WITHSCORES');
  if (lastSend.length === 2) {
    const lastTimestamp = parseInt(lastSend[1]);
    const elapsed = now - lastTimestamp;
    if (elapsed < MIN_INTERVAL_MS) {
      const waitSeconds = Math.ceil((MIN_INTERVAL_MS - elapsed) / 1000);
      return {
        allowed: false,
        reason: `Please wait ${waitSeconds} seconds before requesting another OTP.`,
        retryAfter: waitSeconds,
      };
    }
  }

  // Record the send
  await redis.zadd(key, now, `${now}`);
  await redis.expire(key, Math.ceil(WINDOW_MS / 1000));

  return { allowed: true, remainingSends: MAX_OTP_PER_NUMBER_PER_HOUR - count - 1 };
}

module.exports = { checkPhoneRateLimit };

Integrating into Your Express Handler

const { checkPhoneRateLimit } = require('./otp-rate-limiter');

app.post('/api/send-otp', async (req, res) => {
  const { phone } = req.body;

  // Layer 1: Phone number rate limit
  const rateCheck = await checkPhoneRateLimit(phone);
  if (!rateCheck.allowed) {
    return res.status(429).json({
      error: rateCheck.reason,
      retryAfter: rateCheck.retryAfter,
    });
  }

  // ... remaining layers and OTP send logic
});

This single check stops the bulk of OTP bombing attacks. An attacker cycling through a list of phone numbers can only hit each number 3 times per hour, making large-scale harassment campaigns impractical. For a deeper dive into rate limiting patterns, see our OTP rate limiting guide.


Layer 2: IP + Device Fingerprinting

Per-number rate limiting protects individual phone numbers, but an attacker with a list of 10,000 numbers can still trigger 30,000 OTPs per hour. Layer 2 limits how many OTP requests a single source can make, regardless of the phone numbers used.

IP Rate Limiting Alone Is Not Enough

Sophisticated attackers rotate through hundreds of proxy IPs. A per-IP limit of 10 requests per hour is easily bypassed by cycling through a residential proxy pool. IP limits catch unsophisticated bots but fail against determined attackers.

Combine IP with Device Fingerprinting

A device fingerprint combines browser characteristics (screen resolution, installed fonts, WebGL renderer, timezone, language settings) into a stable identifier. Unlike IP addresses, fingerprints are harder to rotate because they require actually changing the browser or device environment.

// Combined IP + fingerprint rate limiter
async function checkSourceRateLimit(ipAddress, deviceFingerprint) {
  const now = Date.now();
  const hourWindow = 3600000;

  // Per-IP limit: max 10 OTP requests per hour
  const ipKey = `otp:ratelimit:ip:${ipAddress}`;
  await redis.zremrangebyscore(ipKey, 0, now - hourWindow);
  const ipCount = await redis.zcard(ipKey);

  if (ipCount >= 10) {
    return {
      allowed: false,
      reason: 'Too many requests from this network. Try again later.',
    };
  }

  // Per-fingerprint limit: max 15 OTP requests per day
  if (deviceFingerprint) {
    const fpKey = `otp:ratelimit:fp:${deviceFingerprint}`;
    await redis.zremrangebyscore(fpKey, 0, now - 86400000); // 24-hour window
    const fpCount = await redis.zcard(fpKey);

    if (fpCount >= 15) {
      return {
        allowed: false,
        reason: 'Too many requests from this device. Try again tomorrow.',
      };
    }

    await redis.zadd(fpKey, now, `${now}`);
    await redis.expire(fpKey, 86400);
  }

  // Record IP usage
  await redis.zadd(ipKey, now, `${now}`);
  await redis.expire(ipKey, 3600);

  return { allowed: true };
}

Session Binding for Additional Context

Bind OTP requests to an active session token. If an attacker does not have a valid session (because they never loaded your page or app), the request is rejected before the OTP is even generated. This technique pairs well with HMAC session binding for verification integrity.

// Middleware: require a valid session before OTP send
function requireSession(req, res, next) {
  const sessionToken = req.cookies?.otpSession || req.headers['x-otp-session'];

  if (!sessionToken) {
    return res.status(401).json({
      error: 'Invalid session. Please reload the page and try again.',
    });
  }

  // Validate session token (HMAC-signed, time-bound)
  const isValid = verifySessionToken(sessionToken);
  if (!isValid) {
    return res.status(401).json({ error: 'Session expired. Please reload.' });
  }

  next();
}

Layer 3: CAPTCHA Gating

CAPTCHA is the most effective single defence against automated OTP bombing because it breaks the automation loop entirely. The question is not whether to add CAPTCHA — it is where to place it without destroying the user experience for India’s mobile-first audience.

When to Gate: Initial Send vs Resend

ScenarioGate the Initial Send?Gate the Resend?
Low-risk (informational app)No — use invisible challengeYes — after 2nd resend
Medium-risk (e-commerce, food delivery)Invisible challenge (Turnstile)Yes — after 1st resend
High-risk (fintech, payments)Yes — alwaysYes — always

For most Indian consumer apps, the optimal pattern is: invisible CAPTCHA on the initial send, visible CAPTCHA on the second resend attempt. This preserves the tap-and-go experience that Indian mobile users expect while blocking automated abuse.

Server-Side Turnstile Verification

const TURNSTILE_SECRET = process.env.TURNSTILE_SECRET_KEY;

async function verifyCaptcha(token, remoteIp) {
  const response = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        secret: TURNSTILE_SECRET,
        response: token,
        remoteip: remoteIp,
      }),
    }
  );

  const result = await response.json();
  return result.success === true;
}

// In your OTP handler
app.post('/api/send-otp', requireSession, async (req, res) => {
  const { phone, captchaToken } = req.body;

  // Layer 3: CAPTCHA verification
  const captchaValid = await verifyCaptcha(captchaToken, req.ip);
  if (!captchaValid) {
    return res.status(403).json({
      error: 'CAPTCHA verification failed. Please try again.',
    });
  }

  // ... rate limiting, phone validation, OTP send
});

UX Considerations for India’s Mobile-First Users

Indian users on ₹8,000–₹15,000 smartphones with intermittent 4G connectivity face real friction from CAPTCHA challenges. Key optimisations:

  • Use Cloudflare Turnstile or hCaptcha invisible mode — they run the challenge in the background without user interaction for 95%+ of legitimate traffic.
  • Avoid reCAPTCHA v2 image selection — it causes 15–30% drop-off on mobile according to Indian e-commerce conversion data.
  • Preload the CAPTCHA script when the user navigates to the login/signup page, not when they tap “Send OTP.” This eliminates the delay on low-bandwidth connections.
  • Show a clear loading indicator — Indian users on slow networks may see a blank pause after tapping “Send OTP” while the CAPTCHA challenge runs. A spinner with “Verifying…” text prevents rage-taps that trigger duplicate requests.

Layer 4: Phone Number Validation Before Send

Every OTP that fires costs money. Validating the phone number before triggering the SMS send is the cheapest defence — it costs zero per check and eliminates invalid, non-Indian, and known-fraudulent numbers from ever consuming an SMS credit.

Reject Non-Indian and Invalid Prefixes

Indian mobile numbers follow a strict format: +91 followed by 10 digits, where the first digit is 6, 7, 8, or 9. Landline numbers and special service numbers should not receive OTPs.

function validateIndianMobile(phoneNumber) {
  // Normalise: remove spaces, dashes, leading 0
  let cleaned = phoneNumber.replace(/[\s\-()]/g, '');

  // Handle common Indian input formats
  if (cleaned.startsWith('0')) cleaned = '+91' + cleaned.slice(1);
  if (cleaned.startsWith('91') && !cleaned.startsWith('+91')) {
    cleaned = '+' + cleaned;
  }
  if (/^[6-9]\d{9}$/.test(cleaned)) cleaned = '+91' + cleaned;

  // Validate E.164 format for Indian mobile
  const indianMobileRegex = /^\+91[6-9]\d{9}$/;

  if (!indianMobileRegex.test(cleaned)) {
    return {
      valid: false,
      reason: 'Please enter a valid Indian mobile number.',
      normalised: null,
    };
  }

  return { valid: true, reason: null, normalised: cleaned };
}

Block Known Fraud Patterns

Certain number ranges are disproportionately used in OTP bombing campaigns. Maintain a blocklist of prefixes and specific numbers flagged from your own delivery logs:

// Blocklist: numbers flagged from past attacks
const BLOCKED_PREFIXES = new Set([
  // Add prefixes identified from your delivery logs
  // with zero verification rates
]);

const BLOCKED_NUMBERS = new Set([
  // Specific numbers from past incidents
]);

async function checkFraudList(phoneNumber) {
  // Check exact number blocklist
  if (BLOCKED_NUMBERS.has(phoneNumber)) {
    return { allowed: false, reason: 'This number cannot receive OTPs.' };
  }

  // Check prefix blocklist
  const prefix = phoneNumber.slice(0, 7); // +91XXXX
  if (BLOCKED_PREFIXES.has(prefix)) {
    return { allowed: false, reason: 'This number cannot receive OTPs.' };
  }

  // Check if this number has been used in recent bombing (0 verifications)
  const recentSends = await redis.get(`otp:history:${phoneNumber}:sends`);
  const recentVerifies = await redis.get(`otp:history:${phoneNumber}:verifies`);

  if (parseInt(recentSends) > 5 && parseInt(recentVerifies) === 0) {
    return { allowed: false, reason: 'This number cannot receive OTPs.' };
  }

  return { allowed: true };
}

The Complete Protected Handler

Here is the full handler with all four layers integrated:

const express = require('express');
const { checkPhoneRateLimit } = require('./otp-rate-limiter');
const { checkSourceRateLimit } = require('./source-rate-limiter');
const { verifyCaptcha } = require('./captcha');
const { validateIndianMobile, checkFraudList } = require('./phone-validator');

app.post('/api/send-otp', requireSession, async (req, res) => {
  const { phone, captchaToken, fingerprint } = req.body;

  // Layer 4: Phone validation (cheapest check first)
  const phoneCheck = validateIndianMobile(phone);
  if (!phoneCheck.valid) {
    return res.status(400).json({ error: phoneCheck.reason });
  }

  const normalisedPhone = phoneCheck.normalised;

  // Layer 4b: Fraud list check
  const fraudCheck = await checkFraudList(normalisedPhone);
  if (!fraudCheck.allowed) {
    // Return generic error — do not reveal the blocklist
    return res.status(400).json({ error: 'Unable to send OTP. Try again later.' });
  }

  // Layer 3: CAPTCHA verification
  const captchaValid = await verifyCaptcha(captchaToken, req.ip);
  if (!captchaValid) {
    return res.status(403).json({ error: 'Verification failed. Please try again.' });
  }

  // Layer 2: IP + device fingerprint rate limit
  const sourceCheck = await checkSourceRateLimit(req.ip, fingerprint);
  if (!sourceCheck.allowed) {
    return res.status(429).json({ error: sourceCheck.reason });
  }

  // Layer 1: Per-phone-number rate limit
  const phoneRateCheck = await checkPhoneRateLimit(normalisedPhone);
  if (!phoneRateCheck.allowed) {
    return res.status(429).json({
      error: phoneRateCheck.reason,
      retryAfter: phoneRateCheck.retryAfter,
    });
  }

  // All layers passed — generate and send OTP
  const otp = generateSecureOtp();
  await storeOtp(normalisedPhone, otp, req.sessionId);
  await smsProvider.send(normalisedPhone, `Your OTP is ${otp}. Valid for 5 min.`);

  res.json({
    success: true,
    message: 'OTP sent successfully.',
    remainingSends: phoneRateCheck.remainingSends,
  });
});

Note the ordering: the cheapest checks (phone validation, fraud list) run first. CAPTCHA and rate limiting — which involve network calls or Redis queries — run only after the phone number has passed basic validation. This ordering minimises compute cost per rejected request.


Platform-Level Protection: What Your SMS Provider Should Do

App-layer defences are essential, but they only cover your own endpoint. A comprehensive defence also needs platform-level controls from your SMS provider. When evaluating OTP providers, look for:

  • Automatic abuse detection: The provider monitors traffic patterns across all customers and flags anomalous sends before they reach the carrier network.
  • Per-number and per-IP rate limiting at the API level: Server-side enforcement that cannot be bypassed by client-side manipulation.
  • Spend caps with automated pause: Configurable daily limits that halt OTP sends when breached, preventing overnight wallet drain.
  • Direct operator routing: Eliminates the intermediary aggregator layer where grey-route revenue-sharing exploits occur.

StartMessaging implements edge-level abuse filtering that drops sends to flagged number ranges before they hit the operator network. This means fewer bot-triggered OTPs reach the delivery pipeline — and fewer show up on your bill. Spend alerts notify you via webhook or email when daily costs exceed your configured threshold, and you can set hard caps that automatically pause sends. Pair these platform controls with the four app-layer defences above and you cover 99%+ of the OTP bombing attack surface. Get started with StartMessaging — OTP delivery starts at ₹0.25 per message with no DLT registration hassle.


Frequently Asked Questions

Q: Is OTP bombing illegal in India?

A: Yes. OTP bombing can be prosecuted under multiple provisions of Indian law. Section 66 of the Information Technology Act, 2000 covers computer-related offences including causing damage by sending excessive electronic messages. Section 66A (though partially struck down by the Supreme Court in 2015) and the broader IT Act provisions on denial-of-service still apply. Harassment-style OTP bombing — flooding a victim’s phone — can also be charged under Section 354D (stalking) of the Indian Penal Code if the intent is to harass. The DPDP Act 2023 adds a layer: phone numbers are personal data, and processing them without consent (which OTP bombing does) creates DPDP compliance exposure.

Q: Can my SMS provider block OTP bombing automatically?

A: Partially. A good provider can detect anomalous traffic patterns (sudden spikes in sends to numbers with zero verification, sends from flagged IP ranges) and block or throttle at the API level. However, no provider can fully protect you if your endpoint itself has no rate limiting — the requests still reach your server, consume compute resources, and may trigger sends before the provider’s filters kick in. Provider-level controls are Layer 5 in your defence stack, not a replacement for Layers 1–4.

Q: How do I detect if I am being bombed right now?

A: Monitor three signals in real time: (1) a sudden spike in OTP send volume without a corresponding spike in verifications — your send-to-verify ratio will collapse below 30%; (2) a surge in requests from a small number of IP addresses or a single ASN; (3) OTP requests for phone numbers that have never appeared in your user database before. Set up monitoring with Slack or PagerDuty alerts tied to these signals. The Node.js monitoring code in our SMS pumping fraud guide provides a production-ready implementation.

Q: Does implementing all four layers significantly slow down the OTP send flow for legitimate users?

A: No. The phone validation check (Layer 4) is a synchronous string operation — sub-millisecond. The Redis rate limit checks (Layers 1 and 2) add 2–5ms each. The CAPTCHA verification (Layer 3) adds 100–300ms, but if you use an invisible challenge (Cloudflare Turnstile), this happens in the background while the user types their phone number. Total added latency for a legitimate user is under 350ms — imperceptible when the user is waiting for an SMS to arrive.

Q: What about WhatsApp OTP as an alternative to SMS for bombing protection?

A: WhatsApp OTP does not eliminate the bombing vector — an attacker can still hit your endpoint and trigger WhatsApp message sends that cost you money. However, WhatsApp has stronger built-in anti-spam mechanisms at the platform level, which can throttle automated sends more aggressively than SMS networks. The trade-off is coverage: not all Indian users have WhatsApp active on their primary number, especially in tier-3 and rural segments. A multi-channel strategy with SMS as the primary channel and WhatsApp as fallback, combined with the four layers above, is the most resilient approach.


OTP bombing is a solved problem at the engineering level. The four layers — phone number rate limiting, IP and device fingerprinting, CAPTCHA gating, and phone number validation — cover the attack surface comprehensively. The real risk is not technical complexity; it is shipping without these defences because the default “send OTP on POST” handler works fine in development. Implement the layers in the order described (cheapest checks first), monitor your send-to-verify ratio, and set spend alerts. If you want a provider that fights OTP abuse alongside your own defences, StartMessaging includes built-in traffic filtering, spend caps, and direct operator routing that eliminates the intermediary layer attackers exploit.

S

StartMessaging Team

StartMessaging Team

Related posts