Use case

SMS routing with MNP and HLR lookups

For SMS aggregators and CPaaS platforms: route each message by the network that serves the number today, and hold back numbers that are unassigned or unreachable.

Last updated

Three illuminated towers over a city compare number validation, MNP lookup and HLR lookup, each with the question it answers.
Which check answers which question: format, porting or live reachability.

SMS aggregators and CPaaS platforms use MNP and HLR lookups to route each message by the network that serves the number today, not the one its prefix suggests. An MNP lookup returns the current network code for $0.001 per number. An HLR lookup also says whether the number is reachable now, so unassigned and switched-off numbers can be held back.

Why does prefix-based routing fail?

Every mobile number range is allocated to one network. For years, that was enough: read the first digits, look up the network, pick the route. Mobile number portability broke that shortcut. A subscriber who switches operator keeps their number, and from then on a different network serves it.

In the EU, the electronic communications code requires a ported number to be activated within one working day of the date agreed with the subscriber (Directive (EU) 2018/1972, Art. 106). Ports are quick and routine, so any prefix table is wrong for every number that has moved. For an aggregator, that shows up as:

  • Wrong-route cost. Many wholesale rates and direct connections are priced per destination network. A ported number sent down the route for its old network can be billed at a different rate, or cost you an extra hop.
  • Failed or late delivery. Some routes reject traffic for numbers that aren't on the network they terminate to, or deliver it through a slower path.
  • Misleading delivery reports. When "operator A" shows a poor delivery rate, part of that bucket may be numbers that live on operator B. Grouping by the real network gives honest per-network numbers.
  • Wasted sends. Numbers that were disconnected or recycled still get messages, and you pay for every attempt.

The fix is to ask for the network per number, and to know whether the number can receive anything at all.

How does it work?

  1. Normalize. Parse every destination to E.164. Unparseable input comes back as number_status: invalid_number and is never checked or charged.
  2. Look up the network. Call POST /v1/lookup with checks: ["mnp"] for routing, or ["hlr"] where reachability matters. Up to 100 numbers per real-time call.
  3. Map to a route. Read attributes.mcc_mnc (the MCC + MNC code) and look it up in your per-network routing table.
  4. Hold back dead numbers. With HLR, don't send to invalid (not assigned). For unreachable, queue the message or tell the sender, instead of paying for an attempt that will wait or fail.
  5. Fall back on unknown. If a check is unknown or pending, use your default route for that destination. Unknown is free.
  6. Cache and refresh. Keep the answer with checked_at. Within the freshness window, repeat checks come free from your account's cache; set max_age when your routing needs something newer.
  7. Batch where you can. For scheduled campaigns, run the list as a bulk job first (POST /v1/jobs, up to 50,000 numbers per job) and route from the results.

Which checks should you use?

CheckWhat it answersWhen to use itService
mnpWas the number ported, and which network (MCC/MNC) is it on now?Per-message routing and least-cost decisionsMNP lookup
hlrIs the number assigned and reachable right now? Ported? Roaming? Current network?Before time-sensitive traffic such as OTPs, and to clean lists of possibly disconnected numbersHLR lookup
carrierIs it mobile, fixed line, VoIP or toll-free?To drop destinations that can't take SMS before any network lookupCarrier lookup
network.carrier_usCurrent carrier for US and Canadian numbersBulk jobs for North American trafficUS/CA carrier lookup

A common pattern is MNP on every message for routing, plus HLR on OTP traffic, where an unreachable answer lets the sender switch to another channel straight away. HLR queries exist only for mobile numbers, so filter fixed lines first if your traffic is mixed.

Real-time lookups or bulk jobs?

It depends on when you know the destination.

  • Per-message traffic such as OTPs, alerts and API sends arrives one message at a time. Call POST /v1/lookup in the send path, with up to 100 numbers per call if you batch a few hundred milliseconds of traffic. Set wait so a slow network can't hold up the send; a check still pending when the wait ends means "use the default route".
  • Scheduled campaigns give you the full list in advance. Run it as a bulk job, route from the downloaded results, and let the cache answer the same numbers free if the send happens inside the freshness window.
  • Mixed platforms often do both: bulk jobs for campaigns uploaded by customers, real-time lookups for API traffic.

In both modes the same answer shape comes back, so one routing function can handle both.

Example request

With a test key, +447700900006 is a ported number that is reachable, and +447700900002 is assigned but unreachable. See test values.

curl https://api.mobilevalidate.com/v1/lookup \
  -H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+447700900006","+447700900002"],"checks":["hlr"]}'

Runs 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 checks of both rows):

JSON
[
  {
    "number.hlr": {
      "service": "number.hlr", "status": "completed", "registered": true,
      "attributes": {"status": "reachable", "ported": true, "roaming": false, "network": "Test Mobile Two", "mcc_mnc": "23410", "country": "GB"},
      "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-28T22:40:26.718Z",
      "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
    }
  },
  {
    "number.hlr": {
      "service": "number.hlr", "status": "completed", "registered": true,
      "attributes": {"status": "unreachable", "ported": false, "roaming": false, "network": "Test Mobile", "mcc_mnc": "23415", "country": "GB"},
      "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-28T22:40:26.718Z",
      "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
    }
  }
]

The first number was ported and now lives on network 23410, so it takes that network's route, not the route for its original range. The second is on its original network but can't be reached right now, so hold the message or offer another channel. In live mode, both answers are conclusive and billed. For routing only, "checks": ["mnp"] returns porting and mcc_mnc for less.

What should you do with each result?

ResultRouting action
MNP porting: not_portedRoute by the number's original network
MNP porting: portedRoute by the returned mcc_mnc
HLR status: reachableSend on the route for mcc_mnc
HLR status: unreachableQueue and retry later, or offer another channel for OTPs
HLR status: invalidDon't send. Remove the number or report it back to the sender
line_type not mobileDon't send SMS; return an error to the sender
unknown, pending or unsupported_countryUse your default route. Not billed

How much does it cost?

The MNP lookup is $0.001 per number and the HLR lookup $0.005 per number, in real time and in bulk jobs alike. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). An HLR answer of unreachable or invalid is conclusive and billed, because the network did answer. Current rates for every check are on the pricing page.

To judge whether lookups pay for themselves, compare the lookup price with the difference between your route prices, plus the cost of messages you'd send to numbers that are unassigned. The cache helps at scale: traffic to the same number inside the freshness window doesn't pay twice.

How do you stay compliant?

Network lookups describe a person's phone. Run them on traffic you or your customers have a lawful reason to send: transactional messages, OTPs and marketing to people who opted in. A lookup is not consent, and a reachable number is not permission to message it. Our acceptable use policy forbids using lookups to support unsolicited bulk messaging or to scan number ranges. Requests that look like sequential ranges are refused (suspected_enumeration).

Sensitive network data stays out of the answer: no IMSI or other SIM identifiers, no serving switch, no cell or location area, and roaming as true or false only. Numbers from sanctioned countries and regions are never checked and answer unsupported_country for free. People can object to checks of their number through the opt-out form.

What are common mistakes?

  • Running HLR where MNP is enough. If you only need the network for routing, MNP answers that for a fifth of the price.
  • Running both on the same number. HLR already includes porting and the current mcc_mnc.
  • Treating unknown as undeliverable. Unknown means no answer this time. Fall back to your default route; don't drop the message.
  • Keeping answers forever. Numbers are ported again and recycled. Store checked_at and refresh answers older than your routing tolerates.
  • Blocking on pending. Some networks answer slowly. Route with your default and update your table when the answer arrives, or use bulk jobs for campaigns.
  • Confusing delivery reports with lookups. A delivery receipt tells you what happened after sending. A lookup tells you before. See delivery receipts vs HLR lookups.

Frequently asked questions

Why can't I route SMS by the number prefix?

Because of mobile number portability. The prefix shows the network a number range was allocated to, not the network that serves a ported number today. Routing by prefix sends ported numbers down the wrong route, which can cost more, fail or arrive late.

Should I use an MNP lookup or an HLR lookup for routing?

For routing alone, the MNP lookup is enough: it returns whether the number was ported and the current network code (MCC/MNC) at $0.001 per number. Use the HLR lookup ($0.005) when you also need to know whether the number is assigned and reachable right now.

Do I need to run both on the same number?

No. The HLR lookup already includes the porting flag and the current network code. Run MNP for routing on every message, or HLR where reachability matters; running both on one number pays twice for the network answer.

What should I do when a lookup returns unknown?

Fall back to your default route for that destination. Unknown answers are free, so a lookup that can't answer costs nothing and your traffic keeps flowing.

How fresh does a routing answer need to be?

Ports happen every day, so a network answer ages. Repeat checks inside the freshness window are served free from your account's cache. Send max_age with a lower value, or 0 for a fresh billed check, when your routing needs a newer answer.

Does an HLR lookup show where the subscriber is?

No. Roaming is returned as true or false only. Location, cell, serving switch and SIM identifiers such as the IMSI are never returned.

Can I run lookups on my customers' traffic?

Yes, for traffic your customers send lawfully. Your contract with them should cover the lookups, and their messages must still follow opt-in rules. The acceptable use policy forbids using lookups to support unsolicited bulk messaging.

Know before you send.

Tell us about your use case. We review every request and set you up with test and live keys.