Phone validation · Integrations · Python

Integration guide

Validate a phone number in Python.

To validate a phone number in Python, parse it with the phonenumbers library to catch typos and get E.164, then send it to the Trueguard phone validation API with requests for line type, carrier, VoIP type and disposable history. 100 checks a month are free, no card. Working code for both steps below.
Get your free API keyAPI referenceUpdated 13 November 2026

In this guide

Free100 / month
Standard$49.99 / 20,000
Overage$0.0025
Median responseUnder 500 ms

Step 1: check the format with phonenumbers

Free, runs in your Python process, no network call. Install: pip install phonenumbers.

# pip install phonenumbers import phonenumbers from phonenumbers import NumberParseException, PhoneNumberFormat, carrier, timezone n = phonenumbers.parse("+1 800 199 1234", None) phonenumbers.is_possible_number(n) # True (the length fits) phonenumbers.is_valid_number(n) # False (the digits are not in the plan) n = phonenumbers.parse("+1 415 992 9960", None) phonenumbers.is_valid_number(n) # True phonenumbers.number_type(n) # 2, PhoneNumberType.FIXED_LINE_OR_MOBILE carrier.name_for_number(n, "en") # "" timezone.time_zones_for_number(n) # ("America/Los_Angeles",) phonenumbers.format_number(n, PhoneNumberFormat.E164) # "+14159929960", send this to the API # parse() raises on bad input, so catch it before you call the API def to_e164(raw: str) -> str | None: try: n = phonenumbers.parse(raw, None) # None: the input must start with + except NumberParseException: return None # "abc" -> NOT_A_NUMBER, "4159929960" -> INVALID_COUNTRY_CODE if not phonenumbers.is_possible_number(n): return None return phonenumbers.format_number(n, PhoneNumberFormat.E164)

parse() raises on input it cannot read

phonenumbers.parse() raises NumberParseException: "abc" gives NOT_A_NUMBER, and a number with no + and no region gives INVALID_COUNTRY_CODE. Catch it and ask for the number with the country code.
  • is_possible_number() checks length only. +1 800 199 1234 passes it and fails is_valid_number().
  • Store the E.164 string from format_number() and send that to the API.
  • phonenumberslite leaves out the geocoder, carrier and timezone packages.

Where libphonenumber stops

The library knows the numbering plan. It does not know who serves the number now or where it has been listed.

"Do not rely on libphonenumber to determine whether numbers are currently assigned to a specific user and reachable."
QuestionphonenumbersTrueguard API
Is the length possible?is_possible_number(), is_possible_number_with_reason()validity.isPossible
Does it match the numbering plan?is_valid_number(), digits checked against the numbering planvalidity.isValid plus validity.reason (tooShort, unallocatedRange, fictionalRange...)
E.164, international, nationalformat_number() with PhoneNumberFormat.E164, INTERNATIONAL, NATIONALformat.e164, format.international, format.national
Countryregion_code_for_number(), .country_codelocation.countryCode, countryName, callingCode
Region, city, time zonesgeocoder.description_for_number(), timezone.time_zones_for_number(). Not in phonenumberslitelocation.region, location.city, location.timezones
Line typenumber_type(), from the numbering plan. One VOIP value, no fixed or non-fixed splitline.type, plus line.voipType fixed or nonFixed and line.smsCapable
Carriercarrier.name_for_number(), original carrier only. Empty for +14159929960 in our runline.originalCarrier, line.carrierType; line.currentCarrier and line.isPorted for US numbers
Listed on a receive-SMS siteNo fieldrisk.isDisposable (last 30 days), risk.previouslyDisposable (31 to 180 days)
SMS pumping exposureNo fieldrisk.smsDestinationRisk (country), risk.rangeRisk (number block)
Runs whereYour Python process, offlineYour server, one HTTPS call, median under 500 ms

Step 2: call the phone validation API from Python

No SDK. One requests.post call from your server.

Python, in your signup view

# pip install requests import os import requests API = "https://api.trueguard.io/v2/phone/validation" def validate_phone(phone: str) -> dict: res = requests.post( API, headers={"X-API-KEY": os.environ["TRUEGUARD_KEY"]}, json={"phone": phone}, timeout=3, ) res.raise_for_status() return res.json() result = validate_phone("+14159929960") result["risk"]["status"] # "safe" result["line"]["voipType"] # "nonFixed" decide_signup(result) # "step_up"

Run this against your own number on the free tier.

Get your free API key

One header, two body fields

Set X-API-KEY to your key. In the JSON body, phone is required (max 64 chars). country is an ISO-2 code, only read for national format and ignored when the number starts with +.

Fields you will branch on

risk.status

safe, risky, invalid or unknown.

validity.reason

Why a number is invalid or unknown.

line.type

mobile, landline, voip, premiumRate and more.

line.voipType

fixed, nonFixed or unknown, for VoIP lines.

risk.isDisposable

Listed on a receive-SMS site in the last 30 days.

risk.previouslyDisposable

Listed 31 to 180 days ago.

Run +14159929960 before you write any code.

The same endpoint the snippet calls. 5 free checks a day, no key needed.

Or try

Where should the call live?

Notebooks and scripts you share

A key pasted into a notebook, a CLI you distribute or a desktop app leaks with the file. Read it from an environment variable on your server, and run phonenumbers first so a typo never costs a check.

Server-side (recommended)

Call validate_phone in the view or route that creates the account, before you save the row or send the code. Works the same in a Django view or form clean method, a FastAPI route and a Flask view.

What the endpoint returns

{ "phone": "+14159929960", "rawPhone": "+14159929960", "validity": { "isValid": true, "isPossible": true, "reason": null }, "format": { "e164": "+14159929960", "international": "+1 415 992 9960", "national": "(415) 992-9960" }, "location": { "countryCode": "US", "countryName": "United States", "callingCode": "1", "region": "CA", "city": "San Francisco", "timezones": [ "America/Los_Angeles" ] }, "line": { "type": "voip", "voipType": "nonFixed", "smsCapable": true, "originalCarrier": "Lumen", "currentCarrier": "Twilio", "isPorted": true, "carrierType": "cpaasVirtual", "mcc": null, "mnc": null }, "risk": { "status": "safe", "isDisposable": false, "previouslyDisposable": false, "smsDestinationRisk": "low", "rangeRisk": "low", "carrierStirShaken": null, "blockAgeDays": 7756, "complaints90d": null } }

What to do with each risk status

The example number is valid and safe, and the rule below still steps it up: it is a non-fixed VoIP line, ported to Twilio.

risk.status

When and what to do

safe

Valid, not disposable, not premium rate. Allow. Step up non-fixed or unknown VoIP and any previouslyDisposable number. VoIP never changes status.

risky

risk.isDisposable true or line.type premiumRate. Block. Do not send an SMS code to a premium-rate line.

invalid

validity.reason notANumber, invalidCountryCode, tooShort, tooLong, invalidLength, fictionalRange. Reject and ask the user to re-enter with the country code.

unknown

validity.reason unknownRange, unallocatedRange, ambiguousCountry. Step up, for example a code by another channel. For ambiguousCountry, ask for the number starting with +.

def decide_signup(result: dict) -> str: line, risk = result["line"], result["risk"] if risk["status"] == "invalid": return "reject" non_fixed = line["type"] == "voip" and line["voipType"] == "nonFixed" if risk["status"] == "risky" or (non_fixed and risk["previouslyDisposable"]): return "block" if non_fixed or line["voipType"] == "unknown" or risk["status"] == "unknown" or risk["previouslyDisposable"]: return "step_up" return "allow"

The full VoIP rule and rollout: Block VoIP numbers at signup

How do I handle errors in Python?

import logging from requests import HTTPError, RequestException logger = logging.getLogger(__name__) def check_signup_phone(phone: str) -> str: try: return decide_signup(validate_phone(phone)) except RequestException as err: if isinstance(err, HTTPError) and err.response.status_code == 400: return "reject" # bad input: ask the user to re-enter # 401, 403, 429, timeouts and network errors: log and fail open logger.error("Trueguard phone check failed: %s", err) return "allow"

Status

What to do

200

Not an error when validity.reason is set. phone is null and status is invalid or unknown. Counts as one check.

400

phone missing or over 64 chars, unsupported country, malformed JSON. Fix the request. Not counted.

401

"Invalid API key". Check TRUEGUARD_KEY.

403

The plan on this key does not include phone validation. Add the phone plan in the dashboard.

429

Quota for the billing period used up. Fail open and alert.

Timeout

requests raises Timeout after timeout seconds. It is a RequestException, so the sample fails open.

What it costs once you ship

Free

$0 forever

100 checks / month

Overage: n/a

Get started free

Standard

Popular

$49.99 / month

20,000 checks / month

Overage: $0.0025 per check

Get your API key

Custom

Contact us

500,000+ checks / month

Overage: Negotiated

Talk to us

Standard is $49.99 a month for 20,000 checks: $0.0025 a signup at full use. At 30,000 signups a month that is $49.99 + 10,000 x $0.0025 = $74.99. Every 200 response counts as a check, including an invalid number. 400, 401, 403 and 429 do not. Running is_possible_number() before the call means a mistyped number never costs a check. Compare free tiers.

Frequently asked questions

The full field reference lives in the API documentation.

How do I validate a phone number in Python?

Use two steps. Parse the input with the phonenumbers library to catch typos and get the E.164 form, then send that number from your server to the Trueguard phone validation API with requests. It returns line type, VoIP type, carrier and disposable history in one JSON response. 100 checks a month are free with no card, then $49.99 for 20,000.

Is the phonenumbers library enough on its own?

It is enough for format and the numbering plan. Its carrier lookup returns the original carrier, and it has no field for fixed or non-fixed VoIP or for whether a number was listed on a public receive-SMS site. Google's libphonenumber FAQ says not to rely on it to tell whether a number is "currently assigned to a specific user and reachable."

What is the difference between is_possible_number and is_valid_number?

is_possible_number() checks the length for the country. is_valid_number() also checks the digits against the numbering plan. +1 800 199 1234 is possible but not valid.

Why does phonenumbers.parse() raise NumberParseException?

parse() raises when it cannot read the input. "abc" gives NOT_A_NUMBER. A number with no leading + and no region gives INVALID_COUNTRY_CODE. Pass a region such as "US" as the second argument, or ask for the number starting with +.

Can I validate phone numbers with Pydantic?

Yes, for the format step. pydantic-extra-types has PhoneNumberValidator, built on phonenumbers. Annotated[str, PhoneNumberValidator(default_region="US", number_format="E164")] turns "415 992 9960" into "+14159929960". Send that value to the API for line type and risk.

Does the API tell me if the number is in service?

No. There is no live line or HLR test. A valid number matches the numbering plan of its country. To prove the user holds the number, send a verification code after the check.

Does an invalid number cost a check?

Yes, if the API returns it. An unreadable number comes back as 200 with validity.reason set, and every 200 counts. Errors such as 400 and 429 do not count. Filter with is_possible_number() first.

Which Python version do I need?

Python 3.10 or later, the minimum for requests 2.34. phonenumbers 9.0.43 ran on Python 3.11 in our test. There is no Trueguard SDK and you do not need one.

Validate phone numbers from Python today.

100 checks a month, free, no card. The code above runs against the production endpoint.

Get your free API key

No credit card required.

trueguard-logo© 2026 Trueguardinfo@trueguard.io