Collecting phone numbers from your customers is more delicate than you might think

Published in code, product on June 10th, 2023.

...and here I was, thinking that capturing basic data like phone numbers would be a trivial task. Boy, was I wrong with this one.

Never underestimate your users... most of the time they will find a way to break the system.

If you are running any type of digital service, phone numbers may be part of the core personal data you collect for any customer, along attributes like name and e-mail. Usually you may account for certain validation rules for these fields, like meeting a set of minimum characters for a name or making sure the e-mail value is actually a mail address. 

It is not that frequent for a service or app to go the extra mile and actually validate the format of a phone number. If you think about it, which customer would want to provide an incorrect phone number when placing an online order? They wouldn't do that knowingly, but mistakes happen. 

This is a business tech article.

It combines strategic insights for leaders with direct implementation paths for projects, teams or individuals.

Avatar of Rareș Popescu

Rareș Popescu

Product Manager & Full-Stack Engineer

Accepting a plain text value for the phone doesn't normally pose an issue, if the only thing you're doing is to collect data via a contact form, say... for an online inquiry. If you're reviewing yourself the incoming requests, you might figure out, for example, that the user forgot to add a leading 0 and you'd be able to fix that when deciding to give them a call.

Once you start adding even the tiniest automation on top of your data, you might want to consider a proper format validation in place for the phone numbers you collect.

Things could go sideways otherwise: you won't feel it if it happens once or twice, but add any occurrence over that and you'll start losing time and money within any of the following domains:

  • in e-commerce, while fulfilling shipments for online orders

  • in communications, when sending SMS notifications

  • in marketing, when pushing data to newsletter systems

  • in security, if you're implementing user authentication based on the phone number

We've seen this first-hand at The Memory Printing Co., when our logistics provider started having issues with certain shipments due to not being able to reach customers for delivery by phone. Have fun trying to fix that without having to contact your customers directly on any other channel you may have available.

The E.164 format

An international standard format exists for the phone numbers, based on the following pattern: [+][country code][area code][subscriber number].

An example would be +44 20 4567 7467 and I'm pretty sure you've seen or used such a number before. 

Some customers may use this format by default when providing their data, although they usually seem to be the exception from the rule. In most other cases, you'd probably receive the [area code] and the [subscriber number]. That is the moment when problems will start to appear.

Enter: E.164, one of the formats that you'd be able to apply for a vast majority of real life use cases.

Validating phone numbers

There are no less than 195 countries in the world and for each of them you can usually find more than one phone number format applicable. 

This makes it very, very difficult to be able to match all the rules out there in a single system, so you'd need to aim for the best bet. Writing the set of rules for every country from scratch would take a crazy amount of research, time and would make little sense, as you'd most probably end up just reinventing the wheel.

One nice Javascript plugin that matches a lot of common sense criteria for international phone numbers is intl-tel-input. It comes with several nice benefits, like:

  • users are able to select the dial code for their phone number from a comprehensive & pre-defined international list

  • you can scope the set of dial codes down and preselect an element within, if you have the locale available upfront or if you do a geo IP lookup

  • visually, you can collect the phone number in the national format, aiming for the most common experience for your users, all while storing the data in the international format

  • the phone number is stored as a single value, directly in E.164, without you having to use multiple columns in your database

Simply put, this plugin it allows you to move from something like this:

to something like this:

in a very short amount of time.

Implementation

The plugin's documentation is quite extensive and should allow you to include it in several systems or use cases. At its very core, you'll need to:

Import the plugin's assets

Add the following line to import the stylesheets in the header of your project:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@18.1.1/build/css/intlTelInput.css">

and the following to import the scripts in the footer:

<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@18.1.1/build/js/intlTelInput.min.js"></script>

You can either use a CDN or bundle these dependencies directly through tools like Webpack.

Initialize the plugin on your input field

Somewhere along your page you should have the input field for the phone number, which might look something like this:

<input type="text" id="address_phone" name="address_phone" required>

To initialize the plugin, you'll need to integrate the following code onto your page, replacing certain elements, where applicable.

<script type="text/javascript">
    var phoneInputField = document.querySelector("#address_phone");

    if (phoneInputField) {
        const phoneInput = window.intlTelInput(phoneInputField, {
            
            // You'll need this script loaded as well, if you plan to call more advanced functions like isValidNumber()
            utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.1.1/build/js/utils.js",
            
            // Set this if you want to scope the list of countries
            onlyCountries: ['<locale>', '<locale>'], 
            
            // Set this if you want to pre-select any item in the list
            initialCountry: '<locale>',
            
            // Set this false if you only want to show the flag
            separateDialCode: true,
            
            // Define a hidden input that will store the full phone number in the E.164 format
            hiddenInput: 'address_phone_full',
            
            // Here you can localise the country names for the dial codes
            localizedCountries: {
                '<locale>': '<translated_country_name>',
            },
        });
    }
</script>

I've added the main intl-tel-input functions I considered relevant as part of the initialization script and, depending on your scenario, you may consider removing some of these or adding other that are available.

Validate the values

One way of providing visual feedback to the users upon validating the input field is presented in the plugin's documentation. 

That's seems to be a standalone validation scenario, applicable exclusively to the phone numbers. If you are already using an existing library to handle front-end validations for your service, you may want to add the phone number rules as part of the same process, for a unified user experience.

Below you can find an example of an implementation based on jQuery Validate:

Define a new rule for the validator

$.validator.addMethod("intlTelNumber", function(value, element) {
    return this.optional(element) || (phoneInput && phoneInput.isValidNumber());
}, 'The phone number needs to be formatted correctly.');

Set the rule in the validate function

...
rules: {
    address_phone: {
        intlTelNumber: true
    },
},
...

Depending on how you organise your markup, you may have to amend also the errorPlacement, highlight or unhighlight functions to showcase the error notice properly to the users.

Store the data

Now that the field is visible, the dial code can be selected and whenever the value is invalid users get a frontend notice, the last remaining step is to store the request value in the database.

Here you'd actually need to save the value of the hidden input field address_phone_full, which is using the E.164 format, rather than to store the value of the address_phone field you've had visible & interactive in the frontend so far. 

When rendering the page, if you need to pre-fill the phone number with an existing value, you should bind your backend value to the address_phone field (not the address_phone_full) and the intl-tel-input plugin will take over the rest.

Other implementations

You may also be interested in the following implementations of the same concept.