Validate phone numbers before you send an SMS

Every verification text costs money, and a surprising share of the numbers typed into forms were never going to receive it. Normalise first, judge the line second, and only then hand the number to your SMS provider.

Phone numbers fail differently from email addresses. A bad email costs you a bounce. A bad phone number costs you an SMS fee, and when someone scripts your "send code" button against premium-rate ranges, it costs you a lot of SMS fees very quickly. The fraud pattern has a name, SMS pumping, and the defence starts with the same step as every other verification: refuse to act on input you have not understood.

Understanding a phone number means four things: which country it belongs to, whether it is a real number in that country's plan, what kind of line it is, and what its one canonical spelling is. Phone Number Validator answers all four in one call at 10 credits. Disposable Phone Number Checker adds one more answer, at 6 credits: has this number been published on a site that receives SMS for anyone who asks?

Give the validator enough to work with

People type phone numbers in every format: (816) 555-1017, 816.555.1017, +1 816 555 1017, 0044 7911 123456. The validator accepts any of them, but it needs to know which numbering plan to read them in. That comes from one of two places: a leading + with the country calling code, or the country parameter as a two-letter code.

Try it livePhone Number Validator10 credits
GEThttps://api.apiverve.com/v1/phonenumbervalidator
curl "https://api.apiverve.com/v1/phonenumbervalidator?number=%28816%29+555-1017&country=us" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

isValid true, type fixed_line_or_mobile, formatted.e164 +18165551017.

Send a national-format number with no country and the call fails with a clear message: country is required unless the number is in international format (starting with +, URL-encoded as %2B in a query string). That last clause is the bug to watch for. In a query string, a raw + decodes as a space. If you build the URL by string concatenation, +18165551017 arrives as 18165551017 and the number is no longer international. URLSearchParams in Node and the params argument in Python requests both encode it correctly as %2B; hand-built URLs do not.

Where does country come from? In order of reliability: a country selector next to the phone field, the country from the billing or shipping address on the same form, and last, the country of the visitor's IP. Do not default silently to your own country. A UK visitor who types 07911 123456 into a form that assumes us gets a confident wrong answer, as you are about to see.

isValid versus isValidNumber, and what country really means

The response has two validity flags and three country fields, and the differences are the most useful part of it.

  • country is what you sent, uppercased, or null if you sent an international number.
  • detectedCountry is the region the number actually belongs to, as an ISO code.
  • isValid means valid for the country you supplied.
  • isValidNumber means valid in its own detected country, whatever you supplied.

Validate the Italian-format number 06 6982 1234 with country: "it" and the result is isValid: false, isValidNumber: true, detectedCountry: "VA", countryName: "Holy See (Vatican City)". The number is real. Vatican City shares Italy's +39 calling code, and that range belongs to it, not to Italy. Shared calling codes are common: +1 covers the US, Canada and much of the Caribbean, and +44 covers the UK and the Crown Dependencies. +44 7911 123456 comes back with detectedCountry: "GG", Guernsey, and four time zones.

Try it livePhone Number Validator10 credits
GEThttps://api.apiverve.com/v1/phonenumbervalidator
curl "https://api.apiverve.com/v1/phonenumbervalidator?number=%2B44+7911+123456" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

No country sent, so country is null. detectedCountry is GG, a real +44 region that is not GB.

So which flag gates? For a pre-SMS check, isValidNumber. You want to know if the number can exist, and a Vatican or Guernsey number is a perfectly good recipient. Use isValid when the country is a business rule, such as a service you may only offer to residents of one country. Then the mismatch is the signal, and the number goes to review rather than reject, because a Jersey resident with a UK mobile is not a fraudster.

Never store a formatted number from an invalid result

Here is the failure that ends up in databases. Validate the UK mobile 07911 123456 with country: "us":

{
  "country": "US",
  "detectedCountry": null,
  "isValid": false,
  "isValidNumber": false,
  "type": "unknown",
  "formatted": {
    "e164": "+107911123456",
    "international": "+1 07911123456"
  }
}

The formatted block is still populated. It is the input mechanically prefixed with the supplied country code, and +107911123456 is not a phone number anywhere. Code that does user.phone = data.formatted.e164 without checking validity first will write it, and every later system will trust it because it looks canonical. Only read formatted after isValidNumber is true.

Check yourself

A signup form assumes country=us. A visitor types 07911 123456. The response has isValid false and formatted.e164 +107911123456. What should your code do?

Answer the check above to continue.

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales