Skip to content

virtualsms-io/virtual-number-checker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 

Repository files navigation

Virtual Number Checker 🔍

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.

License: MIT Python 3.8+ Node.js 16+

Why This Exists

Many services block virtual/VoIP numbers during SMS verification. This tool helps you:

  1. Detect if a number will be blocked before attempting verification
  2. Choose numbers from countries/carriers that pass verification checks
  3. Debug why a virtual number was rejected

Quick Start

Python

pip install -r requirements.txt
python checker.py +14155551234

Node.js

npm install
node checker.js +14155551234

Features

  • ✅ 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

Python Implementation

#!/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")

Node.js Implementation

/**
 * 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`);
  }
});

When to Use Virtual Numbers

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

Related Resources


License

MIT


Part of the VirtualSMS.io developer toolkit.

About

Python & Node.js tool to detect if a phone number is virtual, VoIP, or real mobile SIM. Carrier lookup + heuristic checks.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages