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, not8 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"}'
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
}
}
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)
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")
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');
}
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)