Why libphonenumber returns FIXED_LINE_OR_MOBILE for US numbers
You parsed a US number, called getNumberType(), and got
FIXED_LINE_OR_MOBILE. Not MOBILE. Not
FIXED_LINE. A shrug.
This is the most-searched complaint about Google's libphonenumber, and the answer is not a bug report. The library is telling you the truth about the data it has, and understanding why turns a frustrating return value into a useful signal about what kind of problem you are actually solving.
What the library is actually saying
libphonenumber works from a metadata file describing number ranges per country. For most of the world, the numbering plan reserves separate ranges for mobile and fixed line, so the range itself carries the answer. Ask about a UK number beginning +447 and the library knows it is mobile, because the UK regulator says that range is mobile.
The library's own FAQ explains FIXED_LINE_OR_MOBILE as covering
ranges that are explicitly defined as being for either, including ranges
described as mostly land-line.
The North American Numbering Plan is the significant case. It does not reserve separate ranges by line type at all. A 917 number can be a mobile or a desk phone, and nothing in the number's shape distinguishes them. There is no metadata to consult, because the regulator never made that distinction at the range level.
So FIXED_LINE_OR_MOBILE is the honest answer. A library that
returned MOBILE for US numbers would be guessing, and it would be
wrong roughly as often as it was right.
Where the answer actually lives
The information does exist. It is just in a different dataset with a different shape.
North American numbers are allocated to carriers in blocks. The first six digits, the NPA and NXX, define a block of 10,000 numbers, and since number pooling those are subdivided into ten thousands-blocks of 1,000. Each block is assigned to a specific operating company, and that assignment is published.
If the block went to a wireless carrier, the number was issued as a mobile number. That is an inference from carrier identity rather than from a reserved range, and it is the mechanism behind every US line-type result you have seen, including the ones you pay for. There is a fuller explanation in how a US phone number is actually allocated.
libphonenumber does not ship that data, and reasonably so. Block assignment files are large, they change continuously, and they exist per-country with different formats and different administrators. Bundling them would turn a compact client-side library into a database distribution problem.
What it looks like in code
Java, though the API is near-identical across the ports. Two numbers, one from a plan that separates line types and one from a plan that does not:
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
// UK: the range itself carries the answer
PhoneNumber uk = util.parse("+447700900123", null);
util.getNumberType(uk); // MOBILE
// US: the plan does not separate them
PhoneNumber us = util.parse("+19172134575", null);
util.getNumberType(us); // FIXED_LINE_OR_MOBILE
util.isValidNumber(us); // true — the range is assignable
Both answers are correct. The second is not a failure to determine the type, it is a correct report that the type is not determined by the number.
Before trusting a carrier answer, check whether the region ports:
PhoneNumberToCarrierMapper carrier = PhoneNumberToCarrierMapper.getInstance();
carrier.getNameForNumber(us, Locale.ENGLISH);
// returns the carrier the BLOCK was allocated to
util.isMobileNumberPortableRegion("US"); // true
// so treat that carrier as historical, not current
That last line is the one worth wiring into your code rather than into a comment. If the region ports, the carrier string is a fact about allocation history, and any business logic that treats it as the current network is building on an assumption the library has already told you is unsafe.
The equivalent guard for line type is simply to branch on the ambiguous case rather than coerce it:
switch (util.getNumberType(n)) {
case MOBILE: // send
case FIXED_LINE: // do not send
case FIXED_LINE_OR_MOBILE: // unknown: send anyway, or resolve it
default: // VOIP, TOLL_FREE, PREMIUM_RATE...
}
Which branch FIXED_LINE_OR_MOBILE should take is a business
decision, not a technical one, and it depends entirely on how many rows you
are processing and what a wasted send costs you.
What libphonenumber is for, and what it is not
The library is unusually clear about its own boundary, and it is worth reading rather than discovering later.
Its definition of a valid number is a range one from which carriers can freely assign numbers to users. Not a number that is in service. Not a number that reaches a person. The FAQ says directly that you should not rely on the library to determine whether numbers are currently assigned and reachable, and notes that products needing that certainty use a verification step, such as sending an SMS with a code.
So the accurate way to describe it:
- Parsing and formatting. Excellent, and the reason most projects adopt it. Turning messy input into E.164 is exactly its job.
- Range validity. Reliable, with the caveat that the metadata simplifies some ranges to keep file size down, which produces a small number of false positives.
- Line type. Reliable where the numbering plan separates by range, unavailable where it does not. The NANP is the large exception.
- Whether the line is live. Out of scope, by design and by explicit statement.
The carrier mapper, and the caveat nobody quotes
libphonenumber ships a separate PhoneNumberToCarrierMapper, and
people reach for it when getNumberType() disappoints. It is worth
knowing what it does in a country with number portability.
The FAQ is explicit: where a region supports mobile number portability, the mapper returns the original carrier for the number. Not the current one.
That is the same limitation every offline dataset has, including ours. Google states it plainly rather than burying it, and the honest reading is that this is a property of the problem rather than a flaw in any particular implementation. Carrier at time of allocation is a published fact. Carrier today is a live query.
The library also exposes
isMobileNumberPortableRegion(), which is the right way to decide
programmatically whether the carrier answer for a given country should be
treated as current or historical.
The false positives nobody warns you about
One more limitation worth knowing, because it explains a class of bug that looks like your code is wrong.
libphonenumber's metadata ships inside the client, so its size matters. To keep the files reasonable, the library simplifies number ranges for some regions, Germany and Austria among them. The FAQ is upfront that this produces a small number of false positives: numbers that ought to be reported invalid come back valid.
The consequence is asymmetric, and that asymmetry is useful. When libphonenumber says a number is invalid, it is almost certainly invalid. When it says valid, it means the number did not fail any check the bundled metadata could apply, which is a weaker statement in the simplified regions than in the precise ones.
So a validity result is a floor rather than a verdict. If your German list passes at a suspiciously high rate, that is the simplification, not a particularly clean list.
The same shape of asymmetry applies to block data, for a different reason. A number in a block that was never allocated cannot ring, and that is deterministic. A number in an allocated block might still be unissued or disconnected, which allocation data cannot see. In both systems the negative result is the stronger one.
Your realistic options
1. Accept the ambiguity and design around it
Often correct, and usually skipped. If you are validating a signup form, you
may not need line type at all. libphonenumber's FAQ notes that SMS can be sent
to MOBILE and FIXED_LINE_OR_MOBILE numbers, so
treating the ambiguous case as sendable is a defensible default for a single
number where the cost of one failed send is trivial.
That reasoning collapses at scale. One undeliverable message is noise; a hundred thousand of them is a deliverability problem, because carriers score senders on failure patterns.
2. Query a lookup API per number
Twilio Lookup, Telnyx, Numverify and others will return line type and current carrier, and for genuinely ambiguous numbers a live query is the only thing that resolves a port.
The constraint is the meter. Twilio Lookup Line Type Intelligence was $0.008 per request when checked in September 2026, so 100,000 numbers is $800 for one pass and the same again when the list is re-exported. For a signup form that is nothing. For a file, it is the whole cost model.
3. Add block allocation data locally
Answer what the published data can answer on your own machine, then send only the genuinely ambiguous remainder to a paid lookup. Most of a raw list is not ambiguous, it is checkably dead: duplicates, malformed rows, and numbers in blocks that were never allocated to anyone.
This is what Phonelint does. It ships 1,881,401 number blocks with the application, covers 206 country codes, and processes 1,000,000 numbers in 21.2 seconds, measured on a mid-range Windows laptop. Nothing is uploaded, because the dataset is local.
The same caveat applies to us as to Google's carrier mapper, and it should be stated with the same directness: line type and carrier come from the block's original assignment, so a ported number can read as its old carrier. We measured how much that costs. On a pre-registered US sample in September 2026, our line-type result agreed with Twilio Lookup 75% of the time and with Veriphone 94%. The figure worth remembering is that those two paid references agreed with each other only 73% of the time, which tells you the ambiguity is in the problem rather than in any one vendor. Method and both sample hashes are in the benchmark write-up.
A sensible pipeline
The three approaches are layers, not alternatives:
- libphonenumber for parsing, normalising to E.164 and rejecting structurally impossible input. It is very good at this and costs nothing.
- Block allocation data for line type and for dropping numbers in unallocated blocks. Free per row once you have it, and it removes the largest share of a messy file.
- A metered lookup for the remainder, where a port or a disconnection would change a decision worth money.
Running them in that order is what keeps the paid step small. Running the paid step first means paying $0.008 to be told that a number with nine digits is not a phone number.
Common questions
What is FIXED_LINE_OR_MOBILE?
A value returned by libphonenumber's getNumberType() meaning
the number sits in a range that the numbering plan does not separate by line
type. It is common for North American numbers, because the NANP does not
reserve distinct ranges for mobile and fixed line. It is a statement about the
numbering plan, not an error.
What is Google's libphonenumber?
An open-source library for parsing, formatting and validating international phone numbers, originally built for Android and now used widely across languages. It ships metadata describing number ranges for every country and is the de facto standard for turning messy phone input into E.164.
Can I use libphonenumber to validate phone numbers?
Yes, for what it defines as validity: whether the number sits in a range carriers can assign from. It does not tell you whether a specific number is in service or reachable, and its own FAQ says not to rely on it for that. For that you need a live lookup or a verification step such as sending a code.
How do I check if a phone number is mobile or landline?
Outside North America, getNumberType() usually answers it,
because most numbering plans separate the two by range. Inside the NANP you
need block allocation data showing which carrier was assigned the number's
block, or a live lookup. No amount of string parsing resolves it, because the
distinction is not encoded in the number.
Does libphonenumber tell me the carrier?
Its PhoneNumberToCarrierMapper does, with an important
qualification: in regions with mobile number portability it returns the
original carrier rather than the current one. Use
isMobileNumberPortableRegion() to decide whether that answer
should be treated as current or historical.
Why does libphonenumber say a number is valid when it does not work?
Because valid means the range is assignable, not that the number is connected. A number can sit in a perfectly good range and never have been issued, or have been disconnected last month. Range validity, block allocation and live status are three different checks, and only the first is what libphonenumber offers.
Try it on a number right now
The free checker on the homepage runs the same engine the desktop app uses: validity, country, line type and carrier, one number at a time.
Check a number free