Use case

OTP and sign-up fraud prevention

Check a phone number's line type, live network status and messenger presence before sending a one-time passcode, so fake and high-risk sign-ups are reviewed first.

Last updated

A snow leopard races through a blue data tunnel past cards for number lookup, line type, live status, spam reputation and a deliverable e-mail.
Move faster with trusted data.

Checking a number before you send a one-time passcode (OTP) tells you whether it looks like a real, reachable mobile line or a likely source of abuse. One request returns line type, live network status and whether the number has a messenger account. Your flow then sends, asks for more verification or refuses, before you pay for a message.

Why check a number before sending a code?

Every OTP you send costs money, and fraudsters know it. In SMS pumping, also called artificially inflated traffic, attackers make your sign-up form send large numbers of codes to number ranges they profit from. Fake sign-ups use throwaway numbers to farm promotions or open accounts in bulk. See SMS pumping and our guide to how SMS pumping works.

A pre-send check gives your risk engine facts to work with:

  • Line type from the carrier lookup: mobile, fixed_line, voip, premium_rate, toll_free and other types. A sign-up with a premium-rate number, or an SMS code sent to a fixed line, is worth a second look.
  • Live network status from the HLR lookup: reachable, unreachable or invalid (not assigned). A code sent to an unassigned number can never be entered; one sent to a switched-off phone arrives too late.
  • Messenger presence, for example a WhatsApp or Telegram account. A number with an account passed that platform's own sign-up verification at some point, which makes it more likely to be in real use.
  • Spam reputation (optional, limited access): reports and regulator actions against the number.

None of these proves identity. Together they help you decide where extra verification is worth its cost.

How does the workflow look?

The check runs between "user entered a number" and "we send a code":

  1. Normalize the number to E.164. The API does this for you and uses default_country for numbers typed in national format.
  2. Call POST /v1/lookup with checks: ["carrier", "whatsapp"] and a short wait, for example 5 seconds. Add hlr where undelivered codes are costly.
  3. Apply the decision table below to the results.
  4. Log the decision and the checked_at time, not the raw response. Keep what you store to a minimum.
  5. If a check is pending or unknown, carry on with your default path. Don't block a user because a check timed out.

Your per-IP and per-number rate limits on the code-sending endpoint still apply. The check adds to those defences and doesn't replace them. The OTP sign-up guard recipe shows the whole flow in code.

Which checks answer which question?

CheckWhat it answersWhen to run itLink
carrierMobile, VoIP, fixed line, premium rate or toll free?Before every first code to a new numberCarrier lookup
hlrIs the number assigned and reachable now?Before codes where a failed delivery is costlyHLR lookup
whatsapp, telegramIs the number registered on this app?Sign-up, as an "in use" signal or to offer app deliveryWhatsApp, Telegram
spam (limited access)Is the number in spam or fraud reports?Sign-up, where enabledSpam reputation

What should you do with each result?

SignalSuggested action
line_type is mobile and a messenger account existsSend the code as usual
line_type is mobile and no messenger accountSend the code. Consider a lower send limit for this number
line_type is voipReview: ask for a second factor or use a different verification method
line_type is premium_rate or shared_costBlock: a consumer is very unlikely to receive a sign-up code on these numbers
line_type is toll_freeReview: some toll-free numbers, for example in the US and Canada, can receive texts, but consumers rarely sign up with one
line_type is fixed_lineOffer a voice call instead of SMS
HLR status: invalidRefuse the number; it isn't assigned
HLR status: unreachableOffer another channel now, such as e-mail or an app the user chose
Spam risk_level is high (if enabled)Review or block, depending on your risk appetite
Any check unknown or pendingFall back to your normal flow; don't block on missing data

These are starting points. Tune them with your own fraud data and review the results regularly.

How much does it cost?

You pay per check, and only for conclusive answers. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). A request with two checks on one number can bill two checks. Real-time prices apply because sign-up needs an answer straight away. See pricing for current rates, including the HLR lookup.

Two things help keep the cost down. Repeat checks of the same number inside the freshness window come from your account's cache for free, which helps when a user taps "resend code". And max_cost puts a hard ceiling on what any single request can cost. Compare the cost of a check with the cost of an SMS to the destinations you serve. The check pays for itself when it stops messages that would have been wasted or abused.

What does a request look like?

This test-mode request checks two numbers. +447700900001 answers with data and +447700900003 simulates a timeout, so you can see both paths.

curl https://api.mobilevalidate.com/v1/lookup \
  -H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+447700900001","+447700900003"],"checks":["carrier","whatsapp"],"wait":5}'

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 items in results):

JSON
[
  {
    "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-09-25T14:28:22.282Z",
      "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-09-25T14:28:22.282Z",
      "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": "UPSTREAM_TIMEOUT", "poll_after_ms": null
    },
    "whatsapp.registered": {
      "service": "whatsapp.registered", "status": "unknown", "registered": null, "attributes": null,
      "confidence": null, "confidence_score": null, "checked_at": null,
      "cached": false, "age_seconds": null, "billed": false, "reason": "UPSTREAM_TIMEOUT", "poll_after_ms": null
    }
  }
]

The first number gets its code as usual. The second falls back to your default path, and neither unknown answer is charged.

What limits and rules apply?

A lookup takes up to 100 numbers and up to 20 checks. The total of numbers × checks can't exceed 2,000. Requests that look like sequential number ranges or generated e-mail lists are rejected. Twenty or more consecutive numbers in one request return suspected_enumeration. Each account also has a daily cap on numbers, which GET /v1/limits reports.

Use the checks only on numbers people give you to sign up or verify. Tell users in your privacy notice that you verify phone numbers to prevent fraud. The results are risk signals, not identity proof, and must not be used for credit, employment, housing or insurance decisions. SMS codes themselves are a restricted authenticator in NIST SP 800-63B, so offer stronger options such as passkeys where you can. People whose numbers were checked can object through the opt-out form.

What are the common mistakes?

  • Blocking on unknown. A timeout is not fraud. Fall back to your normal flow.
  • Dropping rate limits because checks exist. Checks and rate limits cover different attacks. Keep both.
  • Blocking all VoIP. Review or step up instead; genuine users have VoIP numbers.
  • Re-checking on every resend. The cache serves repeats for free inside the freshness window; don't force max_age: 0 except for HLR checks where freshness matters.
  • Ignoring destination countries. If you never have users in a country, a country allow-list stops pumping there more cheaply than any check.

Frequently asked questions

Should I block every VoIP number at sign-up?

Usually not. Many genuine users have VoIP numbers. Treat line_type voip as a reason for extra review or a different verification step, not as a block on its own.

How fast is the check in a sign-up flow?

Real-time checks run on POST /v1/lookup with a wait of up to 30 seconds (default 10). If an answer is not back in time the check returns pending, and your flow can continue with its default path.

What happens when a check returns unknown?

Unknown means no conclusive answer, for example a timeout. It has registered set to null, it is not charged, and your flow should fall back to its normal behaviour instead of blocking the user.

Does this replace rate limiting on my OTP endpoint?

No. Number checks add a signal before you send, but you still need per-IP, per-number and per-country rate limits on the endpoint that sends codes.

Can I use the spam reputation check here?

Spam reputation is in limited access for now (approved customers only) and accepts numbers from all countries except sanctioned ones. Where it is enabled for your account you can add it as an extra signal.

When is the HLR lookup worth it before an OTP?

When a code that doesn't arrive is costly. The HLR lookup says whether the number is assigned and reachable now, so you can offer another verification channel at once for unreachable phones and refuse unassigned numbers. Send max_age 0 for a fresh answer.

What is SMS pumping?

It is fraud in which attackers make your sign-up or login form send large numbers of codes to number ranges they profit from. It is also called artificially inflated traffic. Pre-send checks, rate limits and country controls work together against it.

Does a messenger account prove the user is real?

No. It shows the number passed that platform's own sign-up at some point, which makes it more likely to be in use. It says nothing about who holds the number.

All articles

Know before you send.

Create a free account: approved automatically, usually within 10–15 minutes.

Free to start: check 20 phone numbers and 10 e-mail addresses for real on your account, plus an unlimited test mode. You pay only for conclusive answers, from $20.