# Number checks for OTP providers and SMS resellers

> Check numbers before routing OTP and A2P traffic: live HLR status, current network after porting, line type and messaging-app registration, real time or bulk.

Canonical: https://mobilevalidate.com/use-cases/otp-providers-and-sms-resellers · Last updated: 2026-11-08

![A cell tower sends signal to a SIM card; the line type is identified as mobile rather than landline or VoIP.](https://mobilevalidate.com/images/carrier-and-line-type-lookup.svg)

*Carrier lookup returns the line type (mobile, landline or VoIP) and the network behind the number.*


OTP providers, verification platforms and SMS resellers check a number before routing it so they don't pay for codes and messages that can't arrive. One request returns live network status (HLR), the network a ported number is on now (MNP), the line type, and whether the number is registered on WhatsApp or Telegram for a channel fallback. Checks run in real time per message or in bulk per list, and unknown answers are free.

## Why check numbers before you route traffic?

A provider in the middle of the message path pays for every attempt, whether or not the code is entered. Four things eat margin:

- **Routing by the original prefix.** After a number is ported, its prefix still points at the old network. A message routed that way may take a more expensive path, be rejected or arrive late. The [MNP lookup](/services/mnp-lookup) returns the current `mcc_mnc`, so you route by the network that serves the number today. See [how to check if a number is ported](/blog/how-to-check-if-a-phone-number-is-ported).
- **Numbers that aren't live.** Unassigned numbers can never enter a code, and a switched-off phone receives it after it has expired. The [HLR lookup](/services/hlr-lookup) answers `reachable`, `unreachable` or `invalid` before you send.
- **Lines that can't receive a text.** Fixed lines, some VoIP ranges and premium-rate numbers. The [carrier lookup](/services/carrier-lookup) returns `line_type`, so you can switch to a voice code or refuse the request.
- **Artificial traffic.** In [SMS pumping](/glossary/sms-pumping), attackers make sign-up forms send codes to number ranges they profit from. The traffic lands on the provider first. Our [SMS pumping by country](/blog/sms-pumping-by-country) guide covers where it concentrates.

A check costs a fraction of an international SMS to many destinations. Our [OTP cost by country](/blog/otp-cost-by-country) post compares channel costs per destination, so you can see where a check pays for itself.

## How does the workflow look?

**Real time, per request (OTP and transactional traffic):**

1. Normalize the number to [E.164](/glossary/e164). Use `default_country` for numbers your clients send in national format.
2. Call `POST /v1/lookup` with `checks: ["carrier"]` for every first code to a new number, and add `hlr` for destinations where a failed code is costly. Use a short `wait`, such as 5 seconds.
3. Route by the HLR `mcc_mnc` when `ported` is `true`. If you need routing only, ask for `mnp` instead of `hlr`.
4. If the line is fixed, send a voice code. If the phone is `unreachable` and the number has a WhatsApp or Telegram account, offer app delivery where your client's end user agreed to it.
5. On `pending` or `unknown`, route as you normally would. Never hold a code because a check timed out.

**Bulk, per list (routing tables and campaign traffic):**

1. Run the free `POST /v1/jobs/estimate` on the list. It counts valid, invalid, duplicate, cached and unsupported rows and returns the maximum cost.
2. Create the job with `POST /v1/jobs` and pass the estimate as `max_cost`. Group lists by country where you can: per-country minimums apply (see below).
3. Wait with `GET /v1/jobs/{id}?wait=30` or a `job.completed` [webhook](/docs/webhooks), then download CSV or NDJSON. Each line keeps its `row_no`, so you join results back to your own file by row number.
4. Refresh porting data on a schedule. Cached answers inside the freshness window are free.

Rate limits on your own sending endpoints still apply. Checks add a signal; they don't replace per-number and per-country limits.

## Which checks answer which question?

| Check | What it answers | When to run it | Link |
|---|---|---|---|
| `carrier` | Mobile, fixed line, VoIP, premium rate or toll free? | First code to a new number | [Carrier lookup](/services/carrier-lookup) |
| `mnp` | Was the number ported? Which network serves it now? | Least-cost routing, routing-table refresh | [MNP lookup](/services/mnp-lookup) |
| `hlr` | Is it assigned and reachable now? Plus porting and current network | Costly destinations, old lists, retries | [HLR lookup](/services/hlr-lookup) |
| `whatsapp`, `telegram` | Is the number registered on the app? | Channel fallback for consented app delivery | [WhatsApp](/services/whatsapp-number-check), [Telegram](/services/telegram-number-check) |
| `viber`, `rcs` (bulk only) | Viber account, or an RCS-capable number? | Planning channels for opted-in lists | [Viber](/services/viber-number-check), [RCS](/services/rcs-capability-check) |

To translate an `mcc_mnc` code into a country and network name, use the free [MCC/MNC lookup](/tools/mcc-mnc-lookup). The [routing with MNP and HLR](/use-cases/sms-routing-mnp-hlr) use case goes deeper on route selection.

## What should you do with each result?

| Result | Suggested action |
|---|---|
| `line_type: mobile`, HLR `reachable` | Send the SMS on the normal route |
| HLR `ported: true` or MNP `porting: ported` | Route by the returned `mcc_mnc`, not the prefix |
| HLR `status: unreachable` | Offer a voice code or a consented app channel now; retry SMS later |
| HLR `status: invalid` | Refuse; the number isn't assigned |
| `line_type: fixed_line` | Send a voice code instead of SMS |
| `line_type: voip` | Pass to your client's risk rules; many genuine users have VoIP numbers |
| `line_type: premium_rate` or `shared_cost` | Refuse; a consumer is very unlikely to receive a code there |
| WhatsApp or Telegram `registered: true` | Eligible for app delivery if the end user agreed to it |
| Any check `unknown` or `pending` | Route as normal; unknown is free |

These are starting points. Tune them per destination with your own delivery receipts and conversion data.

## What does a request look like?

This request uses the public sandbox key from the [test values](/docs/test-values) page, so you can run it as is. `+447700900006` is a ported test number, and `+447700900002` is a phone that is switched off.

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

Response (excerpt, test mode: the `checks` of both items in `results`):

```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-11-08T02:49:40.617Z",
      "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
    },
    "network.carrier": {
      "service": "network.carrier", "status": "completed", "registered": true,
      "attributes": {"line_type": "mobile", "carrier": "Test Carrier", "country": "GB"},
      "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-11-08T02:49:40.617Z",
      "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
    },
    "whatsapp.registered": {
      "service": "whatsapp.registered", "status": "completed", "registered": true, "attributes": null,
      "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-11-08T02:49:40.617Z",
      "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-11-08T02:49:40.617Z",
      "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
    },
    "network.carrier": {
      "service": "network.carrier", "status": "unknown", "registered": null, "attributes": null,
      "confidence": null, "confidence_score": null, "checked_at": null,
      "cached": false, "age_seconds": null, "billed": false, "reason": "NO_DATA", "poll_after_ms": null
    },
    "whatsapp.registered": {
      "service": "whatsapp.registered", "status": "completed", "registered": false, "attributes": null,
      "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-11-08T02:49:40.617Z",
      "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
    }
  }
]
```

The first number was ported to the network `23410`, so route the code there, not by its original prefix. The second is assigned but switched off, has no WhatsApp account and no carrier data: offer a voice code or hold the SMS for a retry. The unknown carrier answer is free.

For a list, the free estimate shows what a routing refresh would bill. Five rows with two duplicates (the same number once in national format, with `default_country: "GB"`):

```bash
curl https://api.mobilevalidate.com/v1/jobs/estimate \
  -H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+447700900001", "+447700900002", "+447700900006", "+447700900006", "07700 900001"], "checks": ["hlr", "mnp"], "default_country": "GB"}'
```

```json
{"object": "estimate", "total": 5, "valid": 3, "invalid": 0, "duplicate": 2, "cached": 0, "unsupported": 0,
 "suppressed": 0, "checks": ["number.hlr", "number.mnp"], "checks_total": 10, "billable_max": 0,
 "max_cost": {"amount": "0", "currency": "USD"},
 "breakdown": [
   {"check": "number.hlr", "price_mode": "batch", "reason": null, "checks": 3,
    "unit_price": {"amount": "0.005", "currency": "USD"}, "max_cost": {"amount": "0", "currency": "USD"}},
   {"check": "number.mnp", "price_mode": "batch", "reason": null, "checks": 3,
    "unit_price": {"amount": "0.002", "currency": "USD"}, "max_cost": {"amount": "0", "currency": "USD"}}],
 "unsupported_checks": []}
```

Sandbox keys are never billed, so `max_cost` is 0 here. Only the three unique numbers would be checked. In a real job you would pick `hlr` or `mnp`, not both: the HLR answer already carries porting.

## How much does it cost?

As of 8 November 2026, the live catalog lists these prices per conclusive answer:

| Check | Real time | Bulk |
|---|---|---|
| Carrier lookup (`carrier`) | $0.003 | $0.0015 |
| MNP lookup (`mnp`) | $0.002 | $0.002 |
| HLR lookup (`hlr`) | $0.006 | $0.005 |
| WhatsApp (`whatsapp`) | $0.004 | $0.00025 |
| Telegram (`telegram`) | $0.001 | $0.0003 |
| Viber, RCS | bulk only | $0.00015, $0.0005 |

Current prices are always on the [pricing page](/pricing). Unknown, unsupported-country, timeout, invalid-format and duplicate results are free, and repeat checks inside the freshness window come from your account's cache for free. `max_cost` caps any single request or job.

Bulk jobs have two per-country rules. WhatsApp checks get the bulk price for groups of at least 132 numbers from one country; smaller groups are priced at the real-time price. Bulk-only checks, such as RCS, are billed at least the price of 500 checks for each country that returns an answer. Group your lists by destination to pay the bulk price per number. The estimate lists both cases as `small_batch` and `country_minimum` lines, as described in [bulk jobs](/docs/bulk-jobs).

To try it on real numbers, every account can run 20 phone numbers and 10 e-mail addresses through the real-time checks for free on the dashboard's Try it page. A free [test key](/get-test-key) answers the documented test values for integration work.

## Can you resell checks or pass results to your clients?

Running checks to route, price and protect the traffic you carry is internal business use. Making results or the service available to third parties is different. Section 18.2 of the [Terms](/legal/terms) says you may not resell, license or publish results, or make the service available to others, without our written agreement. That covers, for example, returning line type or HLR status to your clients through your own API, or adding number checks to a verification product you sell.

There is no self-serve reseller program. If you want to offer checks to your own customers, e-mail [sales@mobilevalidate.com](mailto:sales@mobilevalidate.com) with your use case, the checks you need and your expected monthly volume. Committed-volume pricing is available on request.

## What limits and rules apply?

- A real-time lookup takes up to 100 numbers and 20 checks, and numbers × checks can't exceed 2,000.
- A bulk job takes up to 50,000 numbers and e-mails, and rows × checks can't exceed 100,000.
- Requests that look like sequential number ranges are rejected. Twenty or more consecutive numbers in one request return `suspected_enumeration`. Check the numbers your clients' users entered, not generated ranges.
- Each account has a daily cap on numbers, which `GET /v1/limits` reports. Ask sales if your traffic needs a higher cap.
- Numbers from sanctioned countries are never checked and answer `unsupported_country`, free.

## What about consent and compliance?

You check numbers on behalf of your clients, so agree the purpose with them in your contract and data processing terms. The numbers should be ones end users gave to receive a code or a message they expect. Checks are signals about a number, not identity proof. The HLR lookup returns roaming as true or false only and never a location, SIM identifier or network switch address. Messaging-app registration is not consent: deliver codes on an app only where the end user chose that channel. Our [acceptable use policy](/legal/acceptable-use) forbids unsolicited bulk messaging and enumeration. People can object through the [opt-out form](/opt-out). For code wording, the free [OTP message template generator](/tools/otp-message-template-generator) and [SMS character counter](/tools/sms-character-counter) help keep codes to one segment.

## What are the common mistakes?

- **Paying for HLR and MNP on the same number.** The HLR answer includes porting. Use MNP alone when routing is all you need.
- **Routing ported numbers by prefix after the check.** Use the returned `mcc_mnc`; that is the point of the check.
- **Treating `unreachable` as `invalid`.** Unreachable numbers exist; the phone is off or out of coverage. Retry or switch channel, don't delete.
- **Sending small country groups in WhatsApp bulk jobs.** Below 132 numbers per country, the real-time price applies. Batch per destination.
- **Running HLR on fixed lines.** HLR queries exist only for mobile numbers. Filter by line type first on mixed lists.
- **Exposing results to clients without an agreement.** Talk to sales first; section 18.2 of the Terms applies.

## Frequently asked questions

### Which check should an OTP provider run before every code?

Usually the carrier lookup for line type, because it is cheap and catches fixed lines, VoIP and premium-rate numbers. Add the HLR lookup where a code that never arrives is expensive, and a WhatsApp or Telegram check where you offer app delivery as a fallback.

### HLR or MNP for least-cost routing?

For routing alone, the MNP lookup is enough and costs less: it returns porting and the current mcc_mnc. The HLR lookup returns the same network fields plus live status, so use it when you also want to stop sending to unassigned or unreachable numbers. Don't pay for both on one number.

### Can I pass the check results on to my own clients?

Using checks to route and protect traffic you carry is internal use. Showing or selling the results to your clients, or building them into a verification product you offer, needs a written agreement with us under section 18.2 of the Terms. E-mail sales@mobilevalidate.com to set one up.

### Do you have a reseller program?

There is no self-serve reseller or affiliate program. Providers who want to offer checks to their own customers sign a written agreement with us. Contact sales with your use case and expected volume.

### What are the per-country minimums in bulk jobs?

WhatsApp checks get the bulk price for groups of at least 132 numbers from one country; smaller groups are priced at the real-time price. Bulk-only checks such as RCS or iMessage are billed at least the price of 500 checks per country that returns an answer. The free estimate shows both before a job starts.

### Is an unknown or timed-out check charged?

No. Unknown, unsupported-country, timeout, invalid-format and duplicate results are never charged. Route the number as you normally would.

### Does the HLR lookup return the subscriber's location or SIM identifiers?

No. It returns status, ported, roaming as true or false, network, mcc_mnc and country. SIM identifiers, switch addresses and cell data are never returned.
