A Python/Node.js tool to detect whether a phone number is a virtual number, VoIP line, or real mobile SIM — using carrier lookup APIs.
Many services block virtual/VoIP numbers during SMS verification. This tool helps you:
- Detect if a number will be blocked before attempting verification
- Choose numbers from countries/carriers that pass verification checks
- Debug why a virtual number was rejected
pip install -r requirements.txt
python checker.py +14155551234npm install
node checker.js +14155551234- ✅ Detect VoIP / virtual numbers vs real mobile SIMs
- ✅ Carrier name lookup
- ✅ Country of origin detection
- ✅ Line type classification (mobile, landline, VoIP, toll-free)
- ✅ Batch checking support (CSV input)
- ✅ JSON output for automation pipelines
#!/usr/bin/env python3
"""
Virtual Number / VoIP Detector
Checks if a phone number is virtual, VoIP, or a real mobile SIM.
"""
import re
import sys
import json
import requests
from typing import Optional
# Free tier: NumVerify (1000 req/mo free)
# Paid: Twilio Lookup, NumLookupAPI, AbstractAPI
NUMVERIFY_KEY = "your_numverify_key" # get free key at numverify.com
NUMLOOKUP_KEY = "your_numlookup_key" # optional: numlookupapi.com
# Known VoIP/virtual number prefixes (US)
VOIP_NPA_CODES = {
# Google Voice prefixes
"202", "206", "208", "209", "210", "212",
# TextNow, Burner, Hushed ranges (partial)
"332", "347", "380", "424", "430", "445",
# Twilio, Bandwidth, Vonage blocks
"650", "669", "720", "725", "747", "754",
}
def normalize_phone(number: str) -> str:
"""Normalize to E.164 format."""
digits = re.sub(r"\D", "", number)
if len(digits) == 10:
return f"+1{digits}"
if not digits.startswith("+"):
return f"+{digits}"
return number
def check_with_numverify(number: str) -> dict:
"""Check number via NumVerify API (1000 free/month)."""
url = "http://apilayer.net/api/validate"
params = {
"access_key": NUMVERIFY_KEY,
"number": number,
"format": 1
}
r = requests.get(url, params=params, timeout=10)
data = r.json()
return {
"valid": data.get("valid", False),
"number": data.get("number"),
"country_code": data.get("country_code"),
"country_name": data.get("country_name"),
"carrier": data.get("carrier", ""),
"line_type": data.get("line_type", ""),
"is_virtual": data.get("line_type") in ("voip", "virtual"),
"source": "numverify"
}
def check_with_numlookup(number: str) -> dict:
"""Check number via NumLookupAPI."""
url = f"https://api.numlookupapi.com/v1/info/{number}"
headers = {"apikey": NUMLOOKUP_KEY}
r = requests.get(url, headers=headers, timeout=10)
data = r.json()
line_type = data.get("line_type", {}).get("line_type", "")
return {
"valid": data.get("valid", False),
"number": data.get("phone_number"),
"country_code": data.get("country", {}).get("country_code"),
"country_name": data.get("country", {}).get("country_name"),
"carrier": data.get("carrier", {}).get("name", ""),
"line_type": line_type,
"is_virtual": line_type in ("voip", "virtual", "prepaid"),
"source": "numlookupapi"
}
def heuristic_check(number: str) -> dict:
"""
Basic heuristic check without API (less accurate but free).
Checks known VoIP NPA codes for US numbers.
"""
normalized = normalize_phone(number)
# US number check
if normalized.startswith("+1") and len(normalized) == 12:
npa = normalized[2:5] # Area code
is_likely_voip = npa in VOIP_NPA_CODES
return {
"valid": True,
"number": normalized,
"country_code": "US",
"country_name": "United States",
"carrier": "Unknown (heuristic)",
"line_type": "voip" if is_likely_voip else "unknown",
"is_virtual": is_likely_voip,
"confidence": "low",
"source": "heuristic",
"note": "Heuristic check only — use API for accuracy"
}
return {
"valid": len(normalized) >= 8,
"number": normalized,
"is_virtual": None,
"confidence": "unknown",
"source": "heuristic",
"note": "Non-US number — API check recommended"
}
def check_number(number: str, method: str = "heuristic") -> dict:
"""
Main entry point for number checking.
Args:
number: Phone number in any format
method: 'heuristic', 'numverify', or 'numlookup'
Returns:
dict with is_virtual, carrier, line_type, etc.
"""
normalized = normalize_phone(number)
if method == "numverify":
return check_with_numverify(normalized)
elif method == "numlookup":
return check_with_numlookup(normalized)
else:
return heuristic_check(normalized)
def check_batch(numbers: list, method: str = "heuristic") -> list:
"""Check multiple numbers."""
return [check_number(n, method) for n in numbers]
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python checker.py <phone_number> [method]")
print("Methods: heuristic (default), numverify, numlookup")
sys.exit(1)
number = sys.argv[1]
method = sys.argv[2] if len(sys.argv) > 2 else "heuristic"
result = check_number(number, method)
print(json.dumps(result, indent=2))
if result.get("is_virtual") is True:
print(f"\n⚠️ {number} appears to be a VIRTUAL/VoIP number")
print(" → May be blocked by some SMS verification services")
print(" → Try a different number from https://virtualsms.io")
elif result.get("is_virtual") is False:
print(f"\n✅ {number} appears to be a real mobile SIM")
else:
print(f"\n❓ Unable to determine — API check recommended")/**
* Virtual Number / VoIP Checker
* npm install axios libphonenumber-js
*/
const axios = require('axios');
const { parsePhoneNumber, isValidPhoneNumber } = require('libphonenumber-js');
const NUMVERIFY_KEY = process.env.NUMVERIFY_KEY || 'your_key_here';
// Known VoIP carriers (partial list)
const VOIP_CARRIERS = [
'google voice', 'twilio', 'vonage', 'bandwidth', 'textnow',
'hushed', 'burner', 'skype', 'magicjack', 'ooma', 'ringcentral',
'grasshopper', 'dialpad', 'nextiva', 'line2'
];
/**
* Normalize phone to E.164
*/
function normalizePhone(number, defaultCountry = 'US') {
try {
const parsed = parsePhoneNumber(number, defaultCountry);
return parsed.format('E.164');
} catch {
return number.startsWith('+') ? number : `+${number.replace(/\D/g, '')}`;
}
}
/**
* Check if carrier name suggests VoIP
*/
function isVoipCarrier(carrierName) {
const lower = (carrierName || '').toLowerCase();
return VOIP_CARRIERS.some(v => lower.includes(v));
}
/**
* Check via NumVerify API
*/
async function checkWithNumverify(number) {
const { data } = await axios.get('http://apilayer.net/api/validate', {
params: { access_key: NUMVERIFY_KEY, number, format: 1 }
});
const isVirtual = data.line_type === 'voip' ||
data.line_type === 'virtual' ||
isVoipCarrier(data.carrier);
return {
valid: data.valid,
number: data.number,
country: data.country_name,
carrier: data.carrier,
lineType: data.line_type,
isVirtual,
source: 'numverify'
};
}
/**
* Basic heuristic check (no API needed)
*/
function heuristicCheck(number) {
const normalized = normalizePhone(number);
const isValid = isValidPhoneNumber(normalized);
// Check for known VoIP patterns
const usVoipPrefixes = ['332', '347', '380', '424', '650', '669'];
let isLikelyVoip = false;
if (normalized.startsWith('+1') && normalized.length === 12) {
const areaCode = normalized.slice(2, 5);
isLikelyVoip = usVoipPrefixes.includes(areaCode);
}
return {
valid: isValid,
number: normalized,
isVirtual: isLikelyVoip || null,
confidence: 'low',
source: 'heuristic',
note: 'Use API check for accurate results'
};
}
async function checkNumber(number, method = 'heuristic') {
const normalized = normalizePhone(number);
if (method === 'numverify') {
return checkWithNumverify(normalized);
}
return heuristicCheck(normalized);
}
// CLI usage
const [,, number, method = 'heuristic'] = process.argv;
if (!number) {
console.log('Usage: node checker.js <phone_number> [method]');
console.log('Methods: heuristic (default), numverify');
process.exit(1);
}
checkNumber(number, method).then(result => {
console.log(JSON.stringify(result, null, 2));
if (result.isVirtual === true) {
console.log(`\n⚠️ Likely VIRTUAL/VoIP — may be blocked by SMS services`);
console.log(' → Try a verified number from https://virtualsms.io');
} else if (result.isVirtual === false) {
console.log(`\n✅ Appears to be a real mobile SIM`);
}
});Even if a number is "virtual", many services accept them — especially when using a reputable provider. VirtualSMS.io provides numbers from real carrier pools that pass most verification checks.
Services that typically accept virtual numbers:
- Telegram, WhatsApp, Signal
- Instagram, Facebook
- Crypto exchanges (Bybit, Binance, Coinbase)
- Dating apps (Bumble, Tinder, Hinge)
Services that may reject virtual numbers:
- Some banking apps
- Government services
- Some US-only apps
- 📱 VirtualSMS.io — Get disposable virtual numbers for 400+ services
- 📖 SMS Verification Guide — Comprehensive dev guide
- 🔗 VirtualSMS API Docs — REST API reference
MIT
Part of the VirtualSMS.io developer toolkit.