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

Canonical: https://mobilevalidate.com/use-cases/otp-and-signup-fraud · Last updated: 2026-09-25

![A sign-up form with a one-time code; one number passes the checks while a VoIP number and a risky number are stopped.](https://mobilevalidate.com/images/otp-and-signup-fraud-screening.svg)

*Check the number before you send the code, and hold back VoIP and high-risk numbers.*


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](/glossary/sms-pumping).

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

- **Line type** from the [carrier lookup](/services/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](/services/whatsapp-number-check) or [Telegram](/services/telegram-number-check) 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](/glossary/e164). 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?

| Signal | Suggested action |
|---|---|
| `line_type` is `mobile` and a messenger account exists | Send the code as usual |
| `line_type` is `mobile` and no messenger account | Send the code. Consider a lower send limit for this number |
| `line_type` is `voip` | Review: ask for a second factor or use a different verification method |
| `line_type` is `premium_rate` or `shared_cost` | Block: a consumer is very unlikely to receive a sign-up code on these numbers |
| `line_type` is `toll_free` | Review: 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_line` | Offer 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 `pending` | Fall 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](/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.

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

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](/opt-out).

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