The short answer: parse the number with the person's country as a hint, reject it if it isn't a valid range, store it in E.164 (+14155552671), and look at the line type: mobile is what you want for SMS, VoIP and premium-rate numbers deserve a second look.
Why a regex doesn't work
Phone numbers vary by country in length, prefixes and allowed ranges. 07400 123456 is a valid UK mobile; 0740 012 3456 typed by a German user is something else entirely. A regex can only check digits. You need numbering-plan data for every country, which is what Google's libphonenumber provides and what Veqen uses.
The four steps
- Parse with a country hint. People type numbers in their national format. Pass their country (from a dropdown or their IP) so
07400 123456can be read. - Validate.
validmeans the number is in a range a carrier can assign.possibleonly means it has the right length. - Store E.164. One canonical format, accepted by every SMS provider, easy to de-duplicate.
- Check the line type. Mobile for SMS codes. VoIP numbers are cheap to create in bulk. Toll-free and premium-rate numbers are never personal phones.
Code
curl https://veqen-app.com/v1/phone/check \
-H "Authorization: Bearer $VEQEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "07400 123456", "country": "GB"}'
def normalize_phone(raw: str, country: str | None):
r = requests.post(
"https://veqen-app.com/v1/phone/check",
headers={"Authorization": f"Bearer {os.environ['VEQEN_API_KEY']}"},
json={"phone": raw, "country": country},
timeout=5,
).json()
if not r["valid"]:
raise ValueError("That phone number doesn't look right. Check the country and the digits.")
if r["line_type"] in ("premium_rate", "toll_free", "pager"):
raise ValueError("Please enter a personal mobile number.")
return r["e164"], r["line_type"]
const r = await fetch("https://veqen-app.com/v1/phone/check", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VEQEN_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ phone: input, country: "GB" }),
}).then((res) => res.json());
if (!r.valid) throw new Error("Please check the number.");
await db.user.update({ where: { id }, data: { phone: r.e164, phoneType: r.line_type } });
Country formats
Not sure what a number should look like? The phone formats directory shows calling codes, lengths and real examples for 245 countries, like the United Kingdom or Nigeria.
Frequently asked questions
Can you tell me if the number is active?
No. Validation tells you the number is a real, assignable range and what kind of line it is. Only sending an SMS or calling proves someone answers it.
Why does a US number say "fixed_line_or_mobile"?
US and Canadian numbering doesn't separate mobile and landline ranges, so the type can't be known from the number alone.