DEV Community

Alexey D
Alexey D

Posted on

Phone Number Validation is Harder Than You Think (Here's a Free API That Gets It Right)

You'd think validating a phone number is trivial. Regex, right? Something like ^\+?[0-9]{7,15}$ and you're done.

Then your users start entering numbers like +1 (800) 555‑0199, 07911 123456, +49 (0) 30 1234567, or just 8 800 555 35 35. Your regex passes all of them. Your SMS provider rejects half.

The real problem isn't format — it's that phone number rules are different in every country, change when telecoms merge, and a valid-looking number can still be a VoIP burner that will never receive an SMS.

What actually needs checking

When you validate a phone number for real-world use, you need:

  • E.164 normalization — the standard format carriers use (+79001234567, not 8 900 123-45-67)
  • Line type — mobile, landline, VoIP, toll-free, premium-rate. Sending OTPs to a landline is a UX dead-end.
  • Carrier lookup — not always necessary, but useful for fraud detection
  • Geographic data — country, region, timezone offset
  • Fraud signal — VoIP numbers and certain number ranges are disproportionately used for fake signups

Doing this with a library like libphonenumber covers the first two. The rest requires live data.

The API

I've been running a phone validation API that wraps all of this into one call. It's free to try.

Endpoint: POST /validate

curl -X POST https://wesley-twiki-athletics-comment.trycloudflare.com/validate \
  -H "Content-Type: application/json" \
  -d '{"phone": "+79001234567"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "phone": "+79001234567",
  "is_valid": true,
  "formatted": "+7 900 123-45-67",
  "country": "Russia",
  "country_code": "RU",
  "carrier": "MegaFon",
  "line_type": "mobile",
  "timezones": ["Europe/Moscow"],
  "fraud_risk": {
    "score": 12,
    "risk_level": "low"
  },
  "is_voip": false,
  "flags": {
    "voip_number": false,
    "premium_rate": false,
    "invalid_number": false,
    "personal_number": false
  }
}
Enter fullscreen mode Exit fullscreen mode

The fraud_risk.score is 0–100. Anything above 60 is worth a second look — it means the number shares characteristics (VoIP range, recently allocated block, carrier pattern) with numbers commonly used for fake accounts.

Using it in Python

import requests

def validate_phone(phone: str) -> dict:
    r = requests.post(
        "https://wesley-twiki-athletics-comment.trycloudflare.com/validate",
        json={"phone": phone},
        timeout=5
    )
    r.raise_for_status()
    return r.json()

result = validate_phone("+447911123456")

if not result["is_valid"]:
    raise ValueError("Invalid phone number")

if result["is_voip"]:
    # maybe ask for verification, don't block outright
    flag_for_review(result)

if result["fraud_risk"]["score"] > 70:
    require_additional_verification(result)
Enter fullscreen mode Exit fullscreen mode

Batch validation

If you're cleaning a database or processing signups in bulk, there's a batch endpoint:

import requests

numbers = [
    "+79001234567",
    "+447911123456",
    "+12025551234",
    "+49301234567"
]

r = requests.post(
    "https://wesley-twiki-athletics-comment.trycloudflare.com/batch",
    json={"phones": numbers}
)

results = r.json()["results"]
valid = [r for r in results if r["is_valid"]]
voip  = [r for r in results if r["is_voip"]]

print(f"{len(valid)}/{len(numbers)} valid, {len(voip)} VoIP")
Enter fullscreen mode Exit fullscreen mode

Up to 10 numbers per request.

In JavaScript (for signup forms)

async function validatePhone(phone) {
  const res = await fetch('https://wesley-twiki-athletics-comment.trycloudflare.com/validate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone })
  });

  if (!res.ok) throw new Error('Validation failed');
  return res.json();
}

// In your form handler:
const result = await validatePhone(formData.phone);

if (!result.is_valid) {
  showError('Please enter a valid phone number');
} else if (result.is_voip && result.fraud_risk.score > 50) {
  showWarning('We may need to verify this number');
}
Enter fullscreen mode Exit fullscreen mode

When fraud_risk matters

A score above 50 doesn't mean "block the user." It means "this number looks like it came from a number pool rather than a person's SIM card." What you do with that is up to your risk tolerance:

  • Score 0–30: Normal. Proceed.
  • Score 30–60: Worth logging. Consider a CAPTCHA or email backup.
  • Score 60–80: High probability of VoIP or bulk-registered number. Add a verification step.
  • Score 80–100: Almost certainly a burner or auto-generated number. Block or require alternative verification.

Try it

The API is available on RapidAPI with a free tier if you need a stable key and rate limits for production. The endpoint above is for testing.

RapidAPI listing: search "Phone Validator Pro" — or drop a comment and I'll link it directly.

Works with 240+ country codes. Returns in under 200ms for most requests.

Top comments (0)