Normalize a list of phone numbers

Normalize phone numbers to E.164 with Node or Python while preserving the original contact records.

Node · PythonLast tested Download and run ↓
On this page

Run the example

Download Node

normalize-phone-numbers.node.mjs

Download the file, then run these commands in its folder using a macOS or Linux terminal. You need Node 18 or later and a secret API key.

Terminal
npm install @parseapi/sdk
export PARSEAPI_KEY='YOUR_SECRET_API_KEY'
node normalize-phone-numbers.node.mjs

The first run uses the included sample. The instructions below show how to supply your own input.

Need a key? Set up this example

Normalize the list

A contact list can mix local phone numbers with international ones. Normalize them to E.164 format before importing or comparing them: (415) 555-2671 with country US becomes +14155552671. The Node and Python scripts keep every original record beside the result.

Run the download without arguments for its four-record sample, then supply your own JSON list as shown below.

1. Normalize one record

Each record uses the same Phone API call. number is text; country is the record's explicit two-letter country when needed:

JavaScript
const data = await parse.phone(number, country ? { country } : {});
results.push(foldResult(row, data));

International numbers starting with + carry their own calling code. National numbers need a country, so (415) 555-2671 requires US. Without that context, a local number receives country_required. Supply the country recorded with the contact.

2. Supply your own list

Save this as phones.json:

json
[
  { "number": "(415) 555-2671", "country": "US" },
  { "number": "020 7946 0958", "country": "GB" },
  { "number": "+33 1 42 68 53 00" },
  { "number": "123", "country": "US" }
]

Keep numbers as strings so leading zeroes survive. The list repeats the same operation for each record, using one SDK client.

Run your selected implementation with phones.json after its filename. For example:

Terminal
node normalize-phone-numbers.node.mjs phones.json

For Python, use python3 normalize-phone-numbers.python.py phones.json. Both write the same JSON shape. These selected fields show a completed normalization and a completed invalid-number check:

json
[
  { "input": { "number": "(415) 555-2671", "country": "US" }, "phone": "+14155552671", "valid": true, "error": null },
  { "input": { "number": "123", "country": "US" }, "phone": null, "valid": false, "error": null }
]

The full output also retains country, national, and international.

The complete script processes up to 100 records sequentially, with a ten-second timeout and no automatic retries. An authentication, access, or rate-limit error stops later requests.

3. Use the normalized value

JavaScript
return {
  input,
  valid: data.valid,
  phone: data.valid ? data.phone : null,
  country: text(data.country),
  national: data.valid ? text(data.national) : null,
  international: data.valid ? text(data.international) : null,
  error: null
};

Store phone beside input. The sample's first three numbers normalize; 123 returns valid: false and phone: null. A failed request has valid: null and an error, so it cannot be mistaken for an invalid number. Later checks stopped by an account error use not_attempted.

Original order, repeated records, and fields such as contact IDs stay intact. Compare normalized numbers without automatically merging people who share one. Store extensions separately.

Add > normalized.json to write a new output file; never redirect onto the input filename. Exit 0 means checks completed, 2 means some checks need attention, and 1 means the script could not start. Input files must be smaller than 64 KiB.

Normalization checks the numbering plan. It does not establish that a number is connected or belongs to a person.

Complete example

The complete Node source. The script reads your API key from PARSEAPI_KEY.

View and copy the complete source
normalize-phone-numbers.node.mjs
import { readFile, stat } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import { parseAPI, ParseAPIError } from '@parseapi/sdk';

const sample = [
  { number: '(415) 555-2671', country: 'US' },
  { number: '020 7946 0958', country: 'GB' },
  { number: '+33 1 42 68 53 00' },
  { number: '123', country: 'US' }
];

async function readRows() {
  if (process.argv.length > 3) throw new Error('Use: node normalize-phone-numbers.node.mjs [phones.json]');
  if (!process.argv[2]) return sample;
  if ((await stat(process.argv[2])).size > 65536) throw new Error('Use a JSON file smaller than 64 KiB.');
  const rows = JSON.parse(await readFile(process.argv[2], 'utf8'));
  if (!Array.isArray(rows) || rows.length === 0 || rows.length > 100) {
    throw new Error('Supply an array containing 1 to 100 phone records.');
  }
  return rows;
}

function emptyResult(input) {
  return { input, valid: null, phone: null, country: null, national: null, international: null, error: null };
}

function foldResult(input, data) {
  if (!data || typeof data.valid !== 'boolean' ||
      (data.valid && (typeof data.phone !== 'string' || !/^\+[1-9]\d{1,14}$/.test(data.phone)))) {
    return { ...emptyResult(input), error: { code: 'unexpected_response', status: null } };
  }
  const text = (value) => typeof value === 'string' && value.length > 0 ? value : null;
  // example:result
  return {
    input,
    valid: data.valid,
    phone: data.valid ? data.phone : null,
    country: text(data.country),
    national: data.valid ? text(data.national) : null,
    international: data.valid ? text(data.international) : null,
    error: null
  };
  // /example:result
}

export async function normalize(rows, parse) {
  const results = [];
  let stopped = false;

  for (const row of rows) {
    const result = emptyResult(row);
    const number = typeof row?.number === 'string' ? row.number.trim() : '';
    const country = typeof row?.country === 'string' ? row.country.trim().toUpperCase() : undefined;
    if (!number || number.length > 100 || (row?.country != null && !/^[A-Z]{2}$/.test(country ?? ''))) {
      result.error = { code: 'invalid_input', status: null };
      results.push(result);
      continue;
    }
    if (!number.startsWith('+') && !country) {
      result.error = { code: 'country_required', status: null };
      results.push(result);
      continue;
    }
    if (stopped) {
      result.error = { code: 'not_attempted', status: null };
      results.push(result);
      continue;
    }
    try {
      // example:request
      const data = await parse.phone(number, country ? { country } : {});
      results.push(foldResult(row, data));
      // /example:request
    } catch (error) {
      result.error = error instanceof ParseAPIError
        ? { code: error.code, status: error.status }
        : { code: 'request_failed', status: null };
      results.push(result);
      // Fix key/access/allowance errors before spending more attempts on this file.
      if (error instanceof ParseAPIError && [401, 403, 429].includes(error.status)) stopped = true;
    }
  }
  return results;
}

async function main() {
  const rows = await readRows();
  // Reads PARSEAPI_KEY. Keep this secret key on the server.
  const parse = parseAPI(undefined, { timeoutMs: 10000, retries: 0 });
  const results = await normalize(rows, parse);
  process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
  // An invalid number is a completed check. A failed check needs attention.
  if (results.some((row) => row.error !== null)) process.exitCode = 2;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch(() => {
    console.error('Could not start. Check PARSEAPI_KEY and provide a JSON array of 1 to 100 records (under 64 KiB).');
    process.exitCode = 1;
  });
}

Need a key? Set up this server example. For every field and parameter, see Phone API reference.

Build something else

All tutorials →