The HLR lookup sends a live query to a mobile number's home network. It tells you whether the number is reachable right now, whether it has been ported, whether the subscriber is roaming, and which network serves the number today. It runs in real time and in bulk jobs, and unknown answers are free.
What does the HLR lookup tell you?
It tells you whether a mobile number is live on its network at the moment of the query. The answer comes from the network's own subscriber records, not from a static table.
The home network keeps a register of its subscribers, known as the Home Location Register (HLR) or, in newer networks, its successors. The 3GPP network architecture specification defines the HLR as "the location register to which a mobile subscriber is assigned for record purposes such as subscriber information", and notes that in 4G (EPS) the same functionality is provided by the HSS (3GPP TS 23.002 v19.0.0, published by ETSI in October 2025). The HLR lookup glossary entry explains how it works. A query to that register shows whether the number is assigned and whether the subscriber can be reached. That goes further than a carrier lookup, which describes the number from reference data.
The main answer is status:
reachable: the network knows the subscriber and considers them reachable.unreachable: the number exists, but the subscriber can't be reached right now (for example, the phone is switched off or out of coverage). Networks call this "absent subscriber", and callers hear it as "the number you are calling is currently unreachable". See HLR lookup results explained.invalid: the network reports the number as not assigned.unknown: no conclusive answer, for example after a network error or timeout.
Who uses it, and why?
- SMS senders drop numbers that are no longer assigned before a send, and hold back messages to unreachable numbers.
- OTP flows use
unreachableto offer another verification channel straight away instead of waiting for an SMS that won't arrive. - Fraud teams compare
portedand the current network with what a customer told them. - Routing uses
mcc_mncandnetwork, which show the network actually serving a ported number. See number checks for OTP providers and SMS resellers.
See number reachability for the difference between a valid number and a reachable one.
HLR or MNP lookup: which one do you need?
| You want to know | Use | Returns |
|---|---|---|
| Is the phone reachable right now? Is it roaming? | HLR lookup (hlr) | status, ported, roaming, network, mcc_mnc, country |
| Which network is the number on today? Was it ported? | MNP lookup (mnp) | porting, mcc_mnc, country |
| Is it a mobile, a landline or VoIP? | Carrier lookup (carrier) | line_type, carrier, original_carrier, country |
For routing and least-cost decisions, the current network is usually enough, so the MNP lookup is the cheaper choice. Use the HLR lookup when reachability matters, such as before an OTP or to clean a list of numbers that may have been disconnected.
What do you get back?
| Field | Type | Meaning |
|---|---|---|
attributes.status | enum | reachable, unreachable, invalid or unknown (primary answer) |
attributes.ported | boolean | The number has been ported to another network |
attributes.roaming | boolean | The subscriber is roaming. True or false only, never a location |
attributes.network | string | Name of the current network |
attributes.mcc_mnc | string | Current network code (MCC + MNC) |
attributes.country | string | ISO country of the current network |
Never returned: IMSI or any other SIM identifier, the serving switch, cell or location area, or any other detail that could locate a person. We don't store these values either.
How is it billed?
You pay per conclusive answer, at the real-time or bulk price shown on the pricing page. reachable, unreachable and invalid are conclusive answers and are billed. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate).
Two cases look alike but are billed differently. Input the API can't parse as a phone number (number_status: invalid_number) is never checked and never charged. A number the network reports as unassigned (status: invalid) is a conclusive answer and is billed.
Repeat checks of the same number inside the freshness window can come from your account's cache and are free (cached: true, billed: false). Reachability changes quickly, so send max_age: 0 when you need a fresh answer, for example right before an OTP.
What are the limits?
- Mobile numbers only. Landlines and most VoIP numbers have no home-network register to query.
- Up to 100 numbers per real-time lookup and 50,000 per job, and up to 20 checks per request.
- Requests that look like sequential number ranges or generated e-mail lists are rejected (
suspected_enumeration). - Numbers from sanctioned countries and regions are never checked and answer
unsupported_country, which is free. - Some networks answer slowly. A check can come back as
pendingfirst; poll the lookup or use a bulk job, as with other checks.
Country guides
Before you check numbers from a country, our guides explain which ranges are mobile and how porting works: United States, Germany, Australia, Bangladesh, Nepal, Sri Lanka, Thailand, Vietnam, Malaysia, Kenya, Colombia. The full list is in phone numbers by country.
How do you use it responsibly?
An HLR answer is information about a person's phone, not just a number. Check only numbers you have a lawful reason to process, such as your own customers and people who gave you their number. Don't use it to track people or to find out whether someone is travelling. The API returns roaming as true or false only for that reason. The acceptable use policy applies, and people can object through the opt-out form.
Example request
With a test key, +447700900001 returns reachable, …002 unreachable, …003 unknown (timeout), …004 pending and then reachable, …005 unsupported_country, and …006 reachable with ported: true. See test mode.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["hlr"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.lookup({
numbers: ["+447700900001"],
checks: ["hlr"],
});
if (error) console.error(error.code, error.message);
else console.dir(data.results, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({ numbers: ["+447700900001"], checks: ["hlr"] }),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.lookup(numbers=["+447700900001"], checks=["hlr"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/lookup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['hlr'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["hlr"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/lookup", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/lookup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["hlr"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyRuns as pasted with the public sandbox key, which answers the test values only. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
Response (excerpt, test mode: the first item of results):
{
"kind": "phone",
"input": "+447700900001",
"e164": "+447700900001",
"country": "GB",
"number_status": "valid",
"checks": {
"number.hlr": {
"service": "number.hlr",
"status": "completed",
"registered": true,
"attributes": {
"status": "reachable",
"ported": false,
"roaming": false,
"network": "Test Mobile",
"mcc_mnc": "23415",
"country": "GB"
},
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-27T09:12:04.318Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}
},
"test": true
}In live mode, billed is true for this answer. A decision rule in a few lines:
const hlr = result.checks["number.hlr"];
if (hlr.status !== "completed") {
// unknown, pending or unsupported_country: not billed, don't block on it
} else if (hlr.attributes.status === "invalid") {
// not assigned: remove from the list
} else if (hlr.attributes.status === "unreachable") {
// switched off or out of coverage: offer another channel, retry later
}Try it now
Run it here with a test value: the console calls the live API from your browser with the public sandbox key and shows the real JSON and a plain-English verdict.
POST /v1/lookup
Public sandbox key · test values only · never billed
Pick a test value or press Run
Press Run or pick a test value above: the console sends a real request to the API with the public sandbox key and shows the answer here.
No response body.curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["hlr"]}'Check your own numbers: 20 phone numbers and 10 e-mail addresses are checked for real, free, after sign-up. Start free
checks: ["number.hlr"] · Realtime lookup via POST /v1/lookup. Every example runs as pasted: it uses the public sandbox key, which answers the test values for free. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["number.hlr"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.lookup({
numbers: ["+447700900001"],
checks: ["number.hlr"],
});
if (error) console.error(error.code, error.message);
else console.dir(data.results, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: ["+447700900001"],
checks: ["number.hlr"],
}),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.lookup(numbers=["+447700900001"], checks=["number.hlr"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/lookup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['number.hlr'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["number.hlr"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/lookup", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/lookup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["number.hlr"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.body// .mcp.json (Claude Code), ~/.cursor/mcp.json (Cursor) or .vscode/mcp.json ("servers" instead of "mcpServers")
{
"mcpServers": {
"mobilevalidate": {
"type": "http",
"url": "https://mcp.mobilevalidate.com/mcp",
"headers": { "Authorization": "Bearer ${MOBILEVALIDATE_API_KEY}" }
}
}
}
// Then ask your agent, e.g.:
// "Check +447700900001 with number.hlr" → tool: lookup_numbersThe hosted MCP server is live for customers with an agent key (mv_agent_…) or a test key. Or run it locally with npx -y @mobilevalidate/mcp.
Real test-mode result for this check
{
"service": "number.hlr",
"status": "completed",
"registered": true,
"attributes": {
"status": "reachable",
"ported": false,
"roaming": false,
"network": "Test Mobile",
"mcc_mnc": "23415",
"country": "GB"
},
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-27T09:12:04.318Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}What you get
Fields of checks["number.hlr"], generated from the public service catalog.
| Field | Type | Meaning |
|---|---|---|
| registered | boolean | null | true when data was found; null when unknown (not charged). |
| status | enum | completed, pending, unknown, unsupported_country or failed. |
| checked_at | timestamp | When the answer was obtained. |
| attributes.status | enum | Reachability from the home network.reachable · unreachable · invalid · unknown |
| attributes.ported | boolean | Number has been ported to another network. |
| attributes.roaming | boolean | Subscriber is roaming (no location detail). |
| attributes.network | string | Current network name. |
| attributes.mcc_mnc | string | Current network code (MCC+MNC). |
| attributes.country | string | ISO country of the current network. |
Frequently asked questions
What does an HLR lookup cost?
It is priced per conclusive answer, with one price for real-time lookups and one for bulk jobs, both shown on the pricing page. reachable, unreachable and invalid are conclusive answers and are billed. unknown is free.
How is an HLR lookup different from a carrier lookup?
A carrier lookup describes the number from reference data: line type and carrier. An HLR lookup asks the number's home network at the moment of the request, so it can also tell you whether the subscriber is reachable now.
How is it different from the MNP lookup?
The MNP lookup answers one question: was the number ported, and which network is it on now. The HLR lookup adds live reachability and roaming. If you only need the current network for routing, the MNP lookup is enough and costs less.
Does it show where a subscriber is?
No. Roaming is reported as true or false only. Location, cell, serving switch and SIM identifiers such as the IMSI are never returned.
Why is an unreachable answer charged?
Because the network answered. unreachable tells you the number exists but the phone can't be reached now, which is the answer you asked for. Only unknown answers, for example after a network error or timeout, are free.
Does it work for landlines?
No. HLR queries only exist for mobile numbers. Check the line type first with the carrier lookup if your list mixes landlines and mobiles.
What does absent subscriber mean in an HLR lookup?
The number is assigned, but the network can't reach the phone right now, usually because it is switched off or out of coverage. MobileValidate reports it as status unreachable. It is temporary: retry later or use another channel, and don't delete the number after one failure.
Is an HLR lookup still possible on 4G and 5G networks?
Yes. 3GPP's network architecture specification says that in 4G (EPS) the HLR functionality is provided by the HSS, and 5G cores have their own subscriber register. The lookup asks whichever register the home network runs, and the answer fields stay the same.
Is there a free live network status (HLR)?
Yes, to try it on your own data: create a free account and check up to 20 phone numbers for real on the dashboard's Try it page, at no cost. Test mode with documented test values is free and unlimited. After that, a live network status (HLR) costs $0.006 per check in real time ($6.00 per 1,000) and $0.005 in bulk jobs ($5.00 per 1,000), paid from prepaid credit. Unknown and other inconclusive results are always free.


