How to Validate Phone Numbers in E.164 & Block VoIP Burner Lines Before Sending SMS OTPs in Node.js
If you have ever built user authentication with SMS One-Time Passwords (OTPs) using Twilio, AWS SNS, Vonage, or MessageBird, you have probably opened your monthly billing dashboard and experienced an unpleasant shock: Do
If you have ever built user authentication with SMS One-Time Passwords (OTPs) using Twilio, AWS SNS, Vonage, or MessageBird, you have probably opened your monthly billing dashboard and experienced an unpleasant shock:
Dozens of dollars wasted attempting to dispatch SMS messages to copper landlines (FIXED_LINE) that can never receive text messages.
Delivery failure fees caused by missing country codes, local trunk prefixes, or malformed user inputs.
Account takeover and bonus abuse bots weaponizing disposable VoIP numbers (Twilio, Google Voice, TextNow) to spawn fake accounts, drain free credits, and bypass rate limits.
Every failed or bot-bound SMS still costs real moneyβtypically between $0.015 and $0.09 per message. At scale, a bot swarm can incinerate hundreds or thousands of dollars from your OTP delivery budget in minutes.
In this tutorial, we will explore why standard regular expressions fail to prevent this problem, and how you can implement an ultra-fast in-memory phone intelligence check in Node.js before dispatching an SMS.
The Myth of the "Phone Number Regex"
Almost every web application starts with something like this:
// β Warning: This only checks characters, not reality.
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(userInput)) {
throw new Error("Invalid phone number");
}
While this regex satisfies the E.164 length constraint (up to 15 digits), it tells you nothing about whether the number can actually receive an SMS:
It cannot distinguish a mobile phone from a landline: +1 (212) 555-0199 might match the regex perfectly, but if it is an office landline in Manhattan, your SMS provider will try to deliver it, fail, and still charge you the carrier dispatch fee.
It cannot identify virtual VoIP burner lines: Fraudsters generate free VoIP numbers through apps like TextNow or Skype to register duplicate accounts or exploit promotional bonuses.
It does not standardize formatting: A user might enter (55) 1234 5678 or 0412 345 678 without an international calling code. If you pass that directly to an SMS API, it will be rejected.
It ignores quiet hours & TCPA compliance: Without knowing the user's country and corresponding IANA timezones, you risk sending marketing or verification texts at 3:00 AM, violating consumer protection laws like the US TCPA.
To solve this, we need carrier line-type detection, international normalization, and fraud risk scoringβexecuted in milliseconds before touching our SMS provider.
Enter PhoneGuard API
Instead of querying slow external telco registries or maintaining gigabytes of raw numbering plan databases yourself, you can query PhoneGuard API via RapidAPI.
It runs 100% in-memory, executing in less than 2 milliseconds:
Cleans and canonicalizes numbers into standard E.164 (e.g., +523312345678), RFC3966, International, and National.
Flags line types (MOBILE, FIXED_LINE, VOIP, TOLL_FREE, PREMIUM_RATE).
Emits a boolean flag: canReceiveSms: true/false.
Infers official IANA timezones (country.timezones) for scheduling.
Calculates an automated riskScore (0β100) and recommendation: "allow", "review", or "block".
Practical Example: Protecting an Express OTP Endpoint
Let's build a production-ready middleware / controller in Node.js that validates incoming phone numbers before dispatching an SMS OTP.
- Install Dependencies
npm install express dotenv
- Node.js Implementation (
send-otp.js)
import express from "express";
const app = express();
app.use(express.json());
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || "YOUR_RAPIDAPI_KEY";
const PHONEGUARD_HOST = "phoneguard-phone-validation-and-carrier-fraud-detector.p.rapidapi.com";
/**
* Validates a phone number using PhoneGuard API on RapidAPI
*/
async function inspectPhoneNumber(phone, defaultCountry = "US") {
const url = `https://${PHONEGUARD_HOST}/v1/validate`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-RapidAPI-Key": RAPIDAPI_KEY,
"X-RapidAPI-Host": PHONEGUARD_HOST
},
body: JSON.stringify({ phone, defaultCountry })
});
if (!response.ok) {
throw new Error(`PhoneGuard returned HTTP ${response.status}`);
}
return response.json();
}
/**
* POST /api/auth/request-otp
* Verifies phone legitimacy before triggering your SMS gateway
*/
app.post("/api/auth/request-otp", async (req, res) => {
try {
const { phone, countryCode = "US" } = req.body;
if (!phone) {
return res.status(400).json({ error: "Phone number is required." });
}
// 1. Inspect the phone number with PhoneGuard API (< 2ms)
const inspection = await inspectPhoneNumber(phone, countryCode);
if (!inspection.success) {
return res.status(400).json({ error: "Invalid phone number." });
}
const { data, meta } = inspection;
console.log(`[PhoneGuard] Inspected in ${meta.execution_time_ms} ms`);
// 2. β‘ Block high-risk, burner, or fraudulent numbers in ONE line
if (data.fraudAnalysis.recommendation === "block") {
return res.status(400).json({
error: "This phone number cannot be verified.",
reasons: data.fraudAnalysis.reasons
});
}
// 3. Verify if the line type is actually capable of receiving SMS
if (!data.canReceiveSms) {
// Landline or Toll-Free: suggest alternative verification
if (data.lineType === "FIXED_LINE") {
return res.status(400).json({
error: "Landlines cannot receive SMS. Please request a voice call verification.",
lineType: data.lineType
});
}
// VoIP or Virtual Burner Line: flag for manual review or email OTP
return res.status(400).json({
error: "Virtual VoIP numbers are not permitted. Please use a mobile carrier number.",
lineType: data.lineType
});
}
// 4. Retrieve canonical E.164 and IANA timezones for quiet-hours enforcement
const e164Number = data.formats.e164; // e.g., "+14155552671"
const userTimezones = data.country.timezones; // e.g., ["America/New_York", "America/Chicago", ...]
console.log(`Sending SMS OTP to canonical E.164: ${e164Number}`);
console.log(`User country: ${data.country.name} | Timezones: ${userTimezones.join(", ")}`);
// 5. Safe to trigger Twilio / AWS SNS without burning budget!
// await twilioClient.messages.create({ body: 'Your code is 482910', to: e164Number, from: '...' });
return res.status(200).json({
success: true,
message: "OTP dispatched successfully.",
carrierFormat: e164Number
});
} catch (error) {
console.error("OTP Request failed:", error);
return res.status(500).json({ error: "Internal server error." });
}
});
app.listen(3000, () => {
console.log("Server listening on http://localhost:3000");
});
Real Response Payload (< 2ms Latency)
Here is what the real JSON response looks like when validating a phone number:
{
"success": true,
"data": {
"phone": "+14155552671",
"isValid": true,
"isPossible": true,
"lineType": "FIXED_LINE_OR_MOBILE",
"canReceiveSms": true,
"formats": {
"e164": "+14155552671",
"international": "+1 415 555 2671",
"national": "(415) 555-2671",
"rfc3966": "tel:+14155552671"
},
"country": {
"code": "US",
"name": "United States of America",
"callingCode": "+1",
"continent": "North America",
"timezones": [
"America/Adak",
"America/Anchorage",
"America/Boise",
"America/Chicago",
"America/Denver",
"America/Detroit",
"America/Los_Angeles",
"America/New_York",
"Pacific/Honolulu"
],
"primaryTimezone": "America/Adak",
"totalTimezones": 29
},
"fraudAnalysis": {
"riskScore": 15,
"recommendation": "allow",
"reasons": [
"AMBIGUOUS_MOBILE_OR_FIXED_PREFIX",
"SMS_DELIVERY_SUPPORTED"
]
}
},
"meta": {
"execution_time_ms": 0.42,
"timestamp": "2026-11-08T07:45:00.000Z"
}
}
Notice execution_time_ms: 0.42. Because the lookup happens entirely in RAM, adding this layer introduces virtually zero perceivable latency to your user signup flow while saving thousands in failed delivery charges.
Key Takeaways
Never send an SMS to an unvalidated phone number: A regex only checks digitsβit doesn't tell you if the line is a mobile device, a landline, or a bot's virtual VoIP burner.
Always format to E.164: Using data.formats.e164 ensures zero syntax rejections from Twilio or AWS SNS.
Respect user quiet hours: Use data.country.timezones to prevent your automated systems from texting customers in the middle of the night.
Block fraud with one line: Checking if (data.fraudAnalysis.recommendation === "block") protects your promotional credits and user tables against malicious botnets.
Try it on RapidAPI
You can test PhoneGuard API directly in your browser or integrate it into your apps with a free tier of 100 requests every month:
π Test PhoneGuard API on RapidAPI Hub (Free Plan Available)
Happy coding, and stop wasting your SMS budgets! π
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.