Phone validation · Integrations · JavaScript
Integration guide
Validate a phone number in JavaScript and Node.js.
In this guide
Step 1: check the format with libphonenumber-js
Free, runs in the browser and in Node, no network call. Install: npm install libphonenumber-js.
// npm install libphonenumber-js
import parseMin from "libphonenumber-js"; // default: min metadata
import parseMax from "libphonenumber-js/max";
const input = "+1 800 199 1234";
parseMin(input)?.isValid(); // true (min checks length only)
parseMax(input)?.isValid(); // false (max checks the digits)
const phone = parseMax("+1 415 992 9960");
phone?.isPossible(); // true
phone?.isValid(); // true
phone?.getType(); // "FIXED_LINE_OR_MOBILE"
phone?.number; // "+14159929960", send this to the API
// parse failures return undefined, so guard before you call the API
if (!parseMax(userInput)?.isPossible()) {
// show "Add the country code, starting with +"
}The default import only checks length
The defaultmin metadata (about 80 kB) has no digit patterns, so isValid() behaves like isPossible() and getType() returns undefined for most countries. Import libphonenumber-js/max (about 145 kB) for full validation.- In the browser, use
isPossible(). The maintainer prefers it because a strictisValid()"could get stale over time and start rejecting freshly-assigned phone number ranges." - Store
.number(E.164) and send that to the API.
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 | libphonenumber-js | Trueguard API |
|---|---|---|
| Is the length possible? | isPossible(), any build | validity.isPossible |
| Does it match the numbering plan? | isValid(), digits checked only with /max | validity.isValid plus validity.reason (tooShort, unallocatedRange, fictionalRange...) |
| E.164, international, national | .number, .format("INTERNATIONAL"), .format("NATIONAL") | format.e164, format.international, format.national |
| Country | .country, .countryCallingCode | location.countryCode, countryName, callingCode |
| Region, city, time zones | No field | location.region, location.city, location.timezones |
| Line type | getType() with /max or /mobile, from the numbering plan | line.type, plus line.voipType fixed or nonFixed and line.smsCapable |
| Carrier | No field | 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 | Browser or Node, offline | Your server, one HTTPS call, median under 500 ms |
Step 2: call the phone validation API from Node.js
No SDK. One axios POST from your server.
Node.js, in your signup route
// npm install axios
import axios from "axios";
const validatePhone = async (phone) => {
const { data } = await axios.post(
"https://api.trueguard.io/v2/phone/validation",
{ phone },
{ headers: { "X-API-KEY": process.env.TRUEGUARD_KEY }, timeout: 3000 }
);
return data;
};
const result = await validatePhone("+14159929960");
result.risk.status; // "safe"
result.line.voipType; // "nonFixed"
decideSignup(result); // "stepUp"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?
Browser (client-side)
Front-end JavaScript ships your key to every visitor. Run libphonenumber-js in the browser, send the API call from your server.
Server-side (recommended)
Call validatePhone in the signup or OTP route, before you write the account row or send the code. Works the same in Next.js route handlers, Express, NestJS, Cloudflare Workers and Lambda.
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 +.
const decideSignup = ({ line, risk }) => {
if (risk.status === "invalid") {
return "reject";
}
const nonFixed = line.type === "voip" && line.voipType === "nonFixed";
if (risk.status === "risky" || (nonFixed && risk.previouslyDisposable)) {
return "block";
}
if (nonFixed || line.voipType === "unknown" || risk.status === "unknown" || risk.previouslyDisposable) {
return "stepUp";
}
return "allow";
};The full VoIP rule and rollout: Block VoIP numbers at signup
How do I handle errors in Node.js?
const checkSignupPhone = async (phone) => {
try {
return decideSignup(await validatePhone(phone));
} catch (err) {
if (err.response?.status === 400) {
return "reject"; // bad input: ask the user to re-enter
}
// 401, 403, 429, timeouts and network errors: log and fail open
console.error("Trueguard phone check failed", err.message);
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.
Timeoutaxios rejects after timeout ms with no err.response. Fail 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 isPossible() 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 JavaScript?
Use two steps. Parse the input with libphonenumber-js to catch typos and get the E.164 form, then send that number from your Node.js server to the Trueguard phone validation API. 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 libphonenumber-js enough on its own?
It is enough for format and the numbering plan. It has no field for the current carrier, fixed or non-fixed VoIP, or 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."
Why does isValid() return true for a fake number?
The default import uses min metadata, which only checks length, so isValid() acts like isPossible(). Import from libphonenumber-js/max to check the digits against the numbering plan.
Should I use isValid() or isPossible() in the browser?
Use isPossible(). It catches typos without rejecting newly assigned ranges if your bundled metadata is old. The API does the full validity check on the server.
Can I call the phone validation API from React or the browser?
No. The key would ship to every visitor. Add a route on your server that calls the API and returns only the decision your form needs.
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 isPossible() first.
Which Node.js version do I need?
Any current Node.js LTS release. The example uses axios and one POST request. There is no Trueguard SDK and you do not need one.
Validate phone numbers from Node.js today.
100 checks a month, free, no card. The code above runs against the production endpoint.
Get your free API keyNo credit card required.

