The MNP lookup tells you whether a mobile number has been ported to another network, and which network it belongs to now. You get porting (ported or not_ported), the current network code (mcc_mnc) and the country of that network. It works for numbers from any country, in real time and in bulk jobs, at $0.001 per conclusive answer. Unknown answers are free.
What is mobile number portability?
Mobile number portability (MNP) lets a subscriber keep their number when they move to another mobile network. Regulators introduced it to make switching easier. The United States required it for wireless numbers from November 2003, and the EU's electronic communications code requires a port to be completed within one working day of the agreed date. Our MNP glossary entry and portability atlas cover the rules country by country.
The side effect is that a number's prefix no longer tells you its network. The prefix shows which operator the range was allocated to. After a port, a different operator serves the number. Every system that guesses the network from the first digits is wrong for every ported number.
Why does porting matter?
- SMS routing and cost. Many messaging routes and wholesale rates are set per destination network. Route a ported number by its prefix and the message can take a more expensive path, be rejected, or be delivered late. The current
mcc_mnclets you route by the network that actually serves the number. - Delivery analysis. When delivery rates drop for "one operator", ported numbers are often mixed into the wrong bucket. Grouping by the current network gives you honest per-network numbers.
- Fraud checks. In a port-out scam, a criminal moves a victim's number to a network and SIM they control, then receives the victim's one-time passcodes. A number that shows
portedwhen your records say it wasn't, or a current network that differs from the one you stored at sign-up, is a reason to add a step before a password reset or payout. - Data quality. Customer records often store the operator a number was bought from. Refreshing it with the current network keeps segmentation and support routing right.
A port by itself is not a risk signal. Millions of people switch operators. Treat ported as context, and act on a change compared with what you knew before.
MNP lookup or HLR lookup?
MNP lookup (mnp) | HLR lookup (hlr) | |
|---|---|---|
| Question it answers | Was the number ported, and which network is it on now? | Is the phone reachable right now, and on which network? |
| Porting | porting: ported, not_ported | ported: true / false |
| Current network | mcc_mnc, country | network, mcc_mnc, country |
| Live reachability | No | status: reachable, unreachable, invalid |
| Roaming | No | roaming: true / false |
| Price at launch | $0.001 per number | $0.005 per number |
| Typical use | Routing, least-cost messaging, record refresh, port-change checks | OTP pre-checks, cleaning lists of possibly disconnected numbers |
If you need the network for routing, the MNP lookup is enough. If you also need to know whether the phone is switched on or the number is still assigned, use the HLR lookup, which includes the porting answer. There is no need to run both on the same number. The carrier lookup answers a third question, the line type (mobile, landline, VoIP).
What do you get back?
| Field | Type | Meaning |
|---|---|---|
attributes.porting | enum | ported, not_ported or unknown (primary answer) |
attributes.mcc_mnc | string | Code of the network the number belongs to now (MCC + MNC) |
attributes.country | string | ISO 3166-1 alpha-2 country of that network |
registered | boolean or null | true when the answer is conclusive; null when not |
status / reason | enum / string | completed, pending, unknown (e.g. UPSTREAM_TIMEOUT), unsupported_country |
checked_at, cached, billed | — | When the answer was obtained, whether it came from cache, whether it was charged |
The MNP lookup returns no subscriber data: no name, no SIM identifiers, no location. It describes where the number is routed, not who holds it.
How is it billed?
$0.001 per number at launch, for real-time lookups and bulk jobs alike. The pricing page shows the current price. ported and not_ported are conclusive answers and are billed. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). A number that the lookup can't place on any network, for example an invalid or unassigned number, comes back as unknown and is free.
Repeat checks of the same number inside the freshness window can come from your account's cache and are free (cached: true, billed: false). Send max_age: 0 to force a fresh check, for example right before a sensitive account change.
What are the limits?
- Built for mobile numbers, in every country except sanctioned countries and regions, which answer
unsupported_countryfor free. - 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). - A check can come back as
pendingfirst when a network answers slowly. Poll the lookup, or use a bulk job. - The answer describes the number at the time of the check. Numbers can be ported again later, so refresh stored results before decisions that depend on them.
How do you use it responsibly?
Porting data describes a person's phone number. Check only numbers you have a lawful reason to process, such as customers, sign-ups and contacts who gave you their number. Don't use it to profile people or for eligibility decisions like credit, housing or employment. The acceptable use policy applies, and people can object to checks of their number through the opt-out form.
Example request
With a test key, +447700900001 returns not_ported, …006 returns ported, …003 returns unknown, …004 is pending and then not_ported, and …005 returns unsupported_country. See test mode.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900006"],"checks":["mnp"]}'// 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: ["+447700900006"],
checks: ["mnp"],
});
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: ["+447700900006"], checks: ["mnp"] }),
});
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=["+447700900006"], checks=["mnp"])
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' => ['+447700900006'],
'checks' => ['mnp'],
]),
]);
$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":["+447700900006"],"checks":["mnp"]}`)
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" => ["+447700900006"],
"checks" => ["mnp"]
})
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": "+447700900006",
"e164": "+447700900006",
"country": "GB",
"number_status": "valid",
"checks": {
"number.mnp": {
"service": "number.mnp",
"status": "completed",
"registered": true,
"attributes": {
"porting": "ported",
"mcc_mnc": "23410",
"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. Routing by the current network, and flagging a network change against what you stored:
const mnp = result.checks["number.mnp"];
if (mnp.status === "completed") {
const route = routeFor(mnp.attributes.mcc_mnc); // your per-network routing table
if (stored.mcc_mnc && stored.mcc_mnc !== mnp.attributes.mcc_mnc) {
// the number moved since you last checked: add a step before a password reset or payout
}
} else {
// unknown, pending or unsupported_country: not billed; fall back to your default route
}Try it now
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.mnp"]}'// 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.mnp"],
});
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.mnp"],
}),
});
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.mnp"])
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.mnp'],
]),
]);
$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.mnp"]}`)
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.mnp"]
})
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.mnp" → 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.mnp",
"status": "completed",
"registered": true,
"attributes": {
"porting": "not_ported",
"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.mnp"], 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.porting | enum | Ported to another network or not.ported · not_ported · unknown |
| attributes.mcc_mnc | string | Current network code (MCC+MNC). |
| attributes.country | string | ISO country of the current network. |
Frequently asked questions
What is an MNP lookup?
A check that tells you whether a mobile number has been ported away from the network its number range was allocated to, and which network it belongs to now, as an MCC/MNC code and a country. MNP stands for mobile number portability.
What does an MNP lookup cost?
$0.001 per number, in real time and in bulk jobs. ported and not_ported are conclusive answers and are billed. unknown is free, including numbers the lookup can't place on a network. The pricing page always shows the current price.
What is the difference between an MNP lookup and an HLR lookup?
The MNP lookup answers two questions: was the number ported, and which network is it on now. The HLR lookup asks the home network live and adds whether the phone is reachable right now and whether it is roaming. MNP is enough for routing; HLR is for reachability.
Does it tell me when the number was ported?
No. It returns ported or not_ported and the current network, not a porting date. A port on its own is normal: many people switch operators every year.
Which countries does it cover?
Mobile numbers from any country, except sanctioned countries and regions, which are never checked and never billed. When no conclusive answer is available for a number, the result is unknown and free.
Does it work for landlines?
It is built for mobile numbers. Use the carrier lookup to learn the line type first if your list mixes landlines, VoIP and mobiles.

