Normalize a list of phone numbers

Turn a phone list into consistent international numbers with Node or Python. Keep each original record.

Node · PythonLast tested Get the complete example ↓
On this page

Run the example

Download the complete source below. In a macOS or Linux terminal, install the SDK, set your secret API key, then run the file.

Install
npm install @parseapi/sdk
API key
export PARSEAPI_KEY='YOUR_SECRET_API_KEY'
Run
node normalize-phone-numbers.node.mjs
Use this example

Normalize the list

Turn phone records into consistent international numbers. (415) 555-2671 with country US becomes +14155552671. Every result keeps its original record.

Choose Node or Python above, install the SDK, and set PARSEAPI_KEY to a secret key. Run the downloaded script with no arguments to normalize its four-record sample. No input file is needed for that first run.

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. Missing context produces country_required; the script never guesses from your location.

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.

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 extra fields such as IDs remain intact. A shared phone number is not permission to merge two people. Keep extensions separately in your application.

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.

Download Node
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 →