Phone validation · Integrations · Python
Integration guide
Validate a phone number in Python.
In this guide
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 1234passes it and failsis_valid_number().- Store the E.164 string from
format_number()and send that to the API. phonenumbersliteleaves 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."
| Question | phonenumbers | Trueguard 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 plan | validity.isValid plus validity.reason (tooShort, unallocatedRange, fictionalRange...) |
| E.164, international, national | format_number() with PhoneNumberFormat.E164, INTERNATIONAL, NATIONAL | format.e164, format.international, format.national |
| Country | region_code_for_number(), .country_code | location.countryCode, countryName, callingCode |
| Region, city, time zones | geocoder.description_for_number(), timezone.time_zones_for_number(). Not in phonenumberslite | location.region, location.city, location.timezones |
| Line type | number_type(), from the numbering plan. One VOIP value, no fixed or non-fixed split | line.type, plus line.voipType fixed or nonFixed and line.smsCapable |
| Carrier | carrier.name_for_number(), original carrier only. Empty for +14159929960 in our run | line.originalCarrier, line.carrierType; line.currentCarrier and line.isPorted for US numbers |
| Listed on a receive-SMS site | No field | risk.isDisposable (last 30 days), risk.previouslyDisposable (31 to 180 days) |
| SMS pumping exposure | No field | risk.smsDestinationRisk (country), risk.rangeRisk (number block) |
| Runs where | Your Python process, offline | Your 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 keyOne 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.statussafe, risky, invalid or unknown.
validity.reasonWhy a number is invalid or unknown.
line.typemobile, landline, voip, premiumRate and more.
line.voipTypefixed, nonFixed or unknown, for VoIP lines.
risk.isDisposableListed on a receive-SMS site in the last 30 days.
risk.previouslyDisposableListed 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
safeValid, not disposable, not premium rate. Allow. Step up non-fixed or unknown VoIP and any previouslyDisposable number. VoIP never changes status.
riskyrisk.isDisposable true or line.type premiumRate. Block. Do not send an SMS code to a premium-rate line.
invalidvalidity.reason notANumber, invalidCountryCode, tooShort, tooLong, invalidLength, fictionalRange. Reject and ask the user to re-enter with the country code.
unknownvalidity.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
200Not an error when validity.reason is set. phone is null and status is invalid or unknown. Counts as one check.
400phone missing or over 64 chars, unsupported country, malformed JSON. Fix the request. Not counted.
401"Invalid API key". Check TRUEGUARD_KEY.
403The plan on this key does not include phone validation. Add the phone plan in the dashboard.
429Quota for the billing period used up. Fail open and alert.
Timeoutrequests raises Timeout after timeout seconds. It is a RequestException, so the sample fails open.
What it costs once you ship
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 keyNo credit card required.

