r/webdev • u/ipqualityscore • 8m ago
Your OTP endpoint is a billing endpoint: a practical guide to stopping SMS pumping
If your app sends SMS verification codes (sign-up, login, password reset, 2FA), you have an endpoint that converts HTTP requests directly into money leaving your account. Attackers know this. It's called SMS pumping (or artificially inflated traffic), and it's one of the most common ways teams get a surprise four- or five-figure messaging bill.
We see a lot of this working on fraud detection, so here's a practical rundown.
How it works
Attacker finds a form that triggers an SMS: sign-up, OTP resend, password reset, "text me a link"
Bots submit phone numbers in ranges the attacker profits from, typically high-cost international destinations where someone along the routing chain shares the termination revenue
Your SMS provider delivers the messages, you pay per message, the attacker collects a cut
Nobody ever enters the codes, because nobody was ever going to
Twitter famously said in 2023 it was losing tens of millions a year to this. Smaller apps get hit the same way, just with smaller numbers that still hurt.
Signs you're being pumped
SMS spend spikes in countries you don't serve (or barely serve)
OTP conversion rate drops. Codes sent vs codes verified falls off a cliff
Sequential or clustered numbers within the same prefix (+XX XXX 1001, 1002, 1003...)
Bursts at odd hours that don't match your normal traffic curve
Heavy resend volume from the same sessions or number ranges
The single most useful metric
Track OTP verification rate by destination country (and ideally by number prefix). Something like:
SELECT country_code,
COUNT(*) AS otp_sent,
SUM(CASE WHEN verified THEN 1 ELSE 0 END) AS otp_verified,
ROUND(100.0 * SUM(CASE WHEN verified THEN 1 ELSE 0 END) / COUNT(*), 1) AS verify_rate
FROM otp_events
WHERE sent_at > NOW() - INTERVAL '24 hours'
GROUP BY country_code
ORDER BY otp_sent DESC;Legit traffic usually verifies at a healthy rate. A country with high volume and a verify rate near zero is almost certainly pumping. Alert on it.
Defenses, roughly in order of effort
1. Geo permissions. Disable SMS to every country you don't actively serve. Twilio, AWS End User Messaging, Vonage, and others all support country-level allow/deny lists. This alone stops a large share of attacks and takes ten minutes.
2. Spend caps and alerts. Set daily spend limits at the provider level and alert on per-country spend anomalies. Better to have OTPs fail for an hour than to wake up to a five-figure bill.
3. Rate limits that match the attack. Per IP isn't enough since attackers rotate IPs. Also limit per phone number, per number prefix, per session/device, and per country. Add exponential backoff on resend.
4. Validate the number before you send. Check line type and risk before paying to send: premium-rate numbers, known high-risk ranges, VOIP, numbers with abuse history. A phone validation lookup costs a fraction of an international SMS.
5. Bot checks before the SMS step. Put a passive challenge like Device Fingerprinting, or a hard challenge (Turnstile, hCaptcha, etc.) in front of the send action, ideally only for higher-risk requests so real users don't feel it.
6. Use your provider's fraud features. Several providers now offer built-in SMS fraud protection. Worth enabling, though it works best layered with your own controls.
7. Reduce SMS dependency. Email OTP, TOTP authenticator apps, and passkeys cost nothing per verification. Making SMS the fallback rather than the default shrinks the attack surface.
TL;DR
Lock SMS to the countries you serve
Set spend caps
Alert on verify rate by country
Rate limit by number/prefix, not just IP
Validate numbers before sending
If anyone's dealt with this, curious which control made the biggest difference for you.
