Use case

OTP and sign-up fraud prevention

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

Last updated

A sign-up form with a one-time code; one number passes the checks while a VoIP number and a risky number are stopped.A sign-up form with a one-time code; one number passes the checks while a VoIP number and a risky number are stopped.

Checking a number before you send a one-time passcode (OTP) tells you whether the number looks like a real, reachable mobile line or a likely source of abuse. A single request can return the line type, the carrier and whether the number has a messenger account. Your sign-up flow can then send, ask for more verification or refuse, 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.

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.
  • 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, US/CA/DE only): 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.
  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.

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
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.

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.

Example request

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
    }
  }
]

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. People whose numbers were checked can object through the opt-out form.

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 (internal customers only) and covers US, Canadian and German numbers. Where it is enabled for your account you can add it as an extra signal.

All articles

Know before you send.

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