Level 1: Basic format check with regex
A simple regular expression can verify that a number is in valid NANP shape once you have stripped formatting characters.
// Accepts +1 or 1 prefix, optional separators, valid NANP structure
const NANP = /^(?:\+?1[\s.-]?)?\(?([2-9]\d{2})\)?[\s.-]?([2-9]\d{2})[\s.-]?(\d{4})$/;
function isNanpFormat(input) {
return NANP.test(input.trim());
}
isNanpFormat("+1 (416) 555-0123"); // true
isNanpFormat("416-555-0123"); // true
isNanpFormat("123-555-0123"); // false (area code starts with 1)
This tells you the number is shaped correctly. It does not tell you the area code is Canadian or that the number exists.
Level 2: Check the area code is Canadian
Since Canada and the US share +1, you need a list of Canadian area codes to separate them.
const CANADIAN_AREA_CODES = new Set([
"204","226","236","249","250","263","289","306","343","354","365","367",
"368","382","403","416","418","428","431","437","438","450","468","474",
"506","514","519","548","579","581","584","587","604","613","639","647",
"672","683","705","709","742","778","780","782","807","819","825","867",
"873","879","902","905"
]);
function isCanadianNumber(input) {
const digits = input.replace(/\D/g, "").replace(/^1/, "");
if (digits.length !== 10) return false;
return CANADIAN_AREA_CODES.has(digits.slice(0, 3));
}
The same idea in Python:
import re
CANADIAN_AREA_CODES = {
"204","226","236","249","250","289","306","343","365","367","368","403",
"416","418","428","431","437","438","450","474","506","514","519","548",
"579","581","584","587","604","613","639","647","672","705","709","778",
"780","782","807","819","825","867","873","879","902","905",
}
def is_canadian_number(raw: str) -> bool:
digits = re.sub(r"\D", "", raw)
if len(digits) == 11 and digits.startswith("1"):
digits = digits[1:]
return len(digits) == 10 and digits[:3] in CANADIAN_AREA_CODES
Area codes are added from time to time, so keep your list in a data file and check it against the official Canadian Numbering Administrator (cnac.ca) periodically. Treat the lists above as a starting point and verify them before relying on them in production.
Level 3: Use libphonenumber
For production systems, use Google's libphonenumber or a port of it. It knows the numbering rules for every country, handles formatting and can tell you whether a number is plausible and valid.
import phonenumbers
num = phonenumbers.parse("+1 416 555 0123", None)
print(phonenumbers.is_possible_number(num))
print(phonenumbers.is_valid_number(num))
print(phonenumbers.region_code_for_number(num)) # "CA" for Canadian numbers
print(phonenumbers.format_number(num, phonenumbers.PhoneNumberFormat.E164))
import { parsePhoneNumberFromString } from "libphonenumber-js";
const phone = parsePhoneNumberFromString("+1 416 555 0123");
console.log(phone?.isValid(), phone?.country, phone?.format("E.164"));
Note that fictional 555-01XX numbers are often rejected by libphonenumber's strict validity check, because they are reserved and not assigned. If your tests use them, test with is_possible_number or use a mock.
Format versus reachability
Even a number that passes every check may not be in service. Format validation tells you the string is well formed. To confirm a user really owns the number, send a verification code by SMS or a call. Never assume that a valid-looking number belongs to the person entering it.
Validation best practices
- Be generous in what you accept (spaces, dashes, brackets, +1) and strict in what you store (E.164)
- Show clear error messages that tell people what to fix
- Do not require a specific format from users
- Store extensions separately
- Re-validate on the server, not only in the browser
- Write tests with a spread of real-looking formats and known-bad inputs
Test data
You will need sample numbers to test all this. Use the generator for random numbers across Canadian area codes, and read our guide to fake numbers for testing for the safest options.