Tutorials

Validate a list of phone numbers with Python

This tutorial takes a messy list of phone numbers, tidies it, and checks each one with the LookValid phone product using Python and requests. It shows how to read the credit fields, handle errors and rate limits, and what the result does and does not tell you.

By LookValid engineering. Published 2026-10-19.

What the phone check answers

The phone product checks a number against its country's numbering plan and returns the number in E.164, national and international formats, the country, the number type the plan assigns, and for North American numbers the area code and its region. It never contacts a carrier, so it cannot say whether a number is switched on, ported or reachable.

E.164 is the ITU-T recommendation titled "The international public telecommunication numbering plan" 2, and it is the form to store: one unambiguous string per number. A number the plan rejects comes back as found: false with the reason, and costs nothing.

Step 1: normalise the input

The API reads punctuation and a trailing extension on its own, so normalisation here is about hygiene: strip the characters that cause stray rejections, collapse whitespace, and drop duplicates so you do not pay twice for the same number. Letters in the number body are refused by the API, so keep letters out of the cleaned string except for an extension marker.

  • Replace non-breaking spaces and runs of whitespace with a single space.
  • Remove brackets, dots and dashes between digits.
  • Remove spaces between digits, so two spellings of one number become the same string.
  • Keep a leading plus sign: it tells the parser the country code is already there.
  • Pass a two-letter country hint such as US for numbers written without a country code. Without it the check assumes US.

Step 2: call the API from Python

With requests, passing a dict to the json argument encodes the body and sets the content type for you 1. The docs also advise setting timeout on nearly every production request, since without it a call can hang indefinitely 1. Check status_code yourself, because a 4xx or 5xx response is returned rather than raised unless you call raise_for_status() 1.

import os
import re
import time
​
import requests
​
API = "https://YOUR_API_HOST"
​
​
def normalise(raw):
    text = raw.replace("\u00a0", " ")
    text = re.sub(r"[().-]", "", text)
    text = re.sub(r"(?<=\d)\s+(?=\d)", "", text)
    return re.sub(r"\s+", " ", text).strip()
​
​
def lookup(session, phone, country=None):
    payload = {"phone": phone}
    if country:
        payload["country"] = country
    for attempt in range(3):
        r = session.post(API + "/v1/phone", json=payload, timeout=10)
        if r.status_code == 429 and attempt < 2:
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue
        if r.status_code >= 400:
            err = r.json().get("error", {})
            raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
        return r.json()
​
​
def validate(numbers, country="US"):
    session = requests.Session()
    session.headers["Authorization"] = "Bearer " + os.environ["LOOKVALID_API_KEY"]
    seen, results, spent = set(), {}, 0
    for raw in numbers:
        phone = normalise(raw)
        if not phone or phone.lower() in seen:
            continue
        seen.add(phone.lower())
        body = lookup(session, phone, country)
        spent += body["credits_charged"]
        results[phone] = body["data"]["e164"] if body["found"] else body["reason"]
    return results, spent
​
​
print(validate(["+1 (202) 555-0143", "+1 202.555.0143"]))

Normalise, deduplicate and validate a list of numbers

A found number is billed at 1 credit per the catalog, and the loop above skips the second spelling of the same number before it reaches the API.

Step 3: read the result and the credit fields

Fields in a result (captured example for +12025550143)
FieldExampleNotes
data.e164+12025550143Store this form
data.number_typefixed_line_or_mobileFrom the numbering plan, not a carrier
data.area_code_region.nameDistrict of ColumbiaNorth American numbers only
data.carriernullAlways null: no carrier data is used
credits_chargedper catalogAlso in the X-Credits-Charged header
credits_remainingyour balanceAlso in the X-Credits-Remaining header
cache_hittrue or falseTrue when the same lookup was already paid for within the cache window

Fields in a result (captured example for +12025550143)

Errors arrive as an error object with a code such as invalid_input or insufficient_credits; the error reference lists them all. Stop the run on insufficient_credits rather than retrying.

Try it on three numbers

Test one number with curl before you run a list.

curl -X POST https://YOUR_API_HOST/v1/phone \
  -H "Authorization: Bearer $LOOKVALID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+12025550143"}'

One phone lookup

Read the phone product page for every field, try the phone number validator in the browser, or begin with the quickstart.

Sources

  1. Requests documentation: Quickstart, read 2026-10-19
  2. ITU-T Recommendation E.164: The international public telecommunication numbering plan, read 2026-10-19