# How to build a phone number risk score from carrier, line type and reputation signals

> Turn carrier, line type, network status and reputation signals into a phone number risk score with three outcomes, reason codes and calibrated thresholds.

Canonical: https://mobilevalidate.com/blog/how-to-build-a-phone-number-risk-score · Last updated: 2026-09-30

![A semicircle gauge split into allow, step up and block zones, a table of signal points, reason-code pills and a masked number card whose unknown signal scores zero.](https://mobilevalidate.com/images/blog/how-to-build-a-phone-number-risk-score.svg)

*Add up explainable points, keep three outcomes, and never count a missing answer as risk.*


By MobileValidate team (https://mobilevalidate.com/about) · Published: 2026-09-30 · Category: Fraud prevention · Tags: Risk scoring, Fraud prevention, Line type, OTP, Account security

A phone number risk score turns a handful of facts about a number, such as its line type, live network status, porting and reputation, plus your own signals like velocity and IP country, into one decision: allow, step up or block. The best scores are simple, explainable and calibrated on your own traffic. Missing data never counts as risk.

This guide is about the decision layer. If you want to know which signal helps against which attack, start with our phone-number fraud signals field guide. Here we assume you already know the signals and want to combine them without hurting real users.

## What should a phone risk score decide?

A score is only useful if it drives an action. Before you pick any weights, write down the flow it protects and the actions available there. A sign-up, a login code, a phone-number change and a payout carry very different costs of being wrong.

For most flows, three outcomes work better than two:

- **Allow.** The normal path, with your usual rate limits.
- **Step up.** Add friction that a real person can pass: a different verification channel, a document check, a short delay, or a manual review queue.
- **Block.** Only for clear negatives, such as numbers that can't receive the code at all or that match a confirmed abuse pattern.

A block-only design forces you to choose between letting fraud through and turning real people away. The step-up band absorbs the uncertain middle, which is where most of the traffic that looks odd actually sits.

## Rules, points or a model: which should you start with?

Start with rules, add points, and consider a model only when you have the data for it.

| Approach | Good for | Weak spot |
|---|---|---|
| Hard rules | Clear negatives: premium-rate lines, numbers the network reports as unassigned, countries you don't serve | Can't express "a bit suspicious in three ways at once" |
| Weighted points | Combining several weak signals into a step-up decision; easy to explain and tune | Weights are guesses until you calibrate them |
| Statistical model | Large volumes with labelled outcomes (confirmed fraud, confirmed good) | Needs training data, monitoring and explanations; harder to appeal |

Most teams get a long way with rules plus a small points table. Every step-up and review also produces a labelled outcome a model can learn from later.

## Which inputs go into the score?

Keep the inputs few, and let each answer a different question.

- **Line type** from the [carrier lookup](/services/carrier-lookup): `line_type` values such as `mobile`, `fixed_line`, `voip`, `premium_rate` or `toll_free`.
- **Live network status** from the [HLR lookup](/services/hlr-lookup): `status` is `reachable`, `unreachable`, `invalid` (not assigned) or `unknown`.
- **Porting** from the [MNP lookup](/services/mnp-lookup) or the carrier lookup's `original_carrier`, compared with the carrier the user or your records claim.
- **Reputation** from the [spam reputation check](/services/spam-reputation): `risk_level` of `high`, `medium`, `low` or `no_reports`. This service is in limited access and not available self-serve, so design the score to work without it.
- **Your own context:** the country of the IP address and billing address, and velocity: how often this number, device or number prefix appeared in the last hour or day.

Each input answers about the number, never about the person. None of them identifies who holds the phone, and the score shouldn't pretend otherwise.

## What does a worked points table look like?

Here is a starting point for a sign-up with an SMS code. **The weights are illustrative, not recommendations.** They show the shape of a table; your own data should set the numbers.

| Signal | Condition | Points | Reason code |
|---|---|---|---|
| Line type | `premium_rate`, `shared_cost` or `uan` | Hard block | `LINE_SPECIAL_RATE` |
| Network status | `invalid` (not assigned) | Hard block (ask to correct) | `NUMBER_NOT_ASSIGNED` |
| Line type | `voip` | +15 | `LINE_VOIP` |
| Line type | `fixed_line` in an SMS flow | Route to voice, no points | `LINE_FIXED` |
| Network status | `unreachable` | +10 | `NETWORK_UNREACHABLE` |
| Porting | ported and current carrier differs from the one the user named | +10 | `CARRIER_MISMATCH` |
| Country | number country ≠ IP country and ≠ billing country | +15 | `COUNTRY_MISMATCH` |
| Velocity | number seen on 3+ accounts in 24 h | +25 | `NUMBER_VELOCITY` |
| Velocity | device or prefix burst above your baseline | +20 | `BURST` |
| Reputation | `risk_level: high` (where enabled) | +30 | `REPUTATION_HIGH` |
| Reputation | `risk_level: medium` (where enabled) | +10 | `REPUTATION_MEDIUM` |
| Any check | `unknown`, `pending`, `unsupported_country` | 0 | none |

With thresholds such as *0–29 allow, 30–59 step up, 60+ manual review or block*, a VoIP number alone (15) is allowed. A VoIP number with a country mismatch and a burst (50) gets a second factor. Several strong signals together (60+) go to review.

Two design choices matter more than the exact numbers. First, no single weak signal crosses a threshold on its own. Second, hard blocks are rules, not points, so they stay rare and easy to defend.

A sketch in code:

```js
// r = one result of POST /v1/lookup; ctx = your own signals. Unknown results add nothing.
function phoneRisk(r, ctx) {
  const c = r.checks ?? {};
  const done = (k) => c[k]?.status === "completed" ? c[k].attributes ?? {} : null;
  const car = done("network.carrier"), hlr = done("number.hlr"), rep = done("number.spam");
  if (["premium_rate", "shared_cost", "uan"].includes(car?.line_type)) return { action: "block", reasons: ["LINE_SPECIAL_RATE"] };
  if (hlr?.status === "invalid") return { action: "correct", reasons: ["NUMBER_NOT_ASSIGNED"] };
  const hits = [];
  if (car?.line_type === "voip") hits.push(["LINE_VOIP", 15]);
  if (hlr?.status === "unreachable") hits.push(["NETWORK_UNREACHABLE", 10]);
  if (ctx.countryMismatch) hits.push(["COUNTRY_MISMATCH", 15]);
  if (ctx.numberAccounts24h >= 3) hits.push(["NUMBER_VELOCITY", 25]);
  if (rep?.risk_level === "high") hits.push(["REPUTATION_HIGH", 30]);
  const score = hits.reduce((s, [, p]) => s + p, 0);
  const action = score >= 60 ? "review" : score >= 30 ? "step_up" : "allow";
  return { action, score, reasons: hits.map(([code]) => code) };
}
```

## Why must unknown count as no signal?

Because an unknown answer says nothing about the number. A timeout, a network that doesn't answer or a country with no data is a fact about the check, not about the user. If unknown added points, customers in countries with thinner data would be stepped up more often for no reason.

So treat every inconclusive answer as if the check hadn't run: zero points, default rules. MobileValidate doesn't charge for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate), so there is no cost reason to count them either. Numbers from sanctioned countries and regions are never checked and come back as `unsupported_country`; your own country rules decide what happens to them.

## Why do reason codes matter?

Every decision should carry the list of reasons that produced it, as in the `reasons` array above. Reason codes do three jobs:

- **Support.** An agent can tell a user "we need a second step because the number couldn't be reached" instead of "you were flagged".
- **Appeals.** A person who contests a decision can be reviewed against specific reasons, and a reviewer can override one signal without rewriting the score.
- **Tuning.** Counting outcomes per reason code shows which signals catch fraud and which mostly catch good users.

Store the reason codes and the check results' `checked_at` times with the decision. Don't store more of the raw lookup than you need; see our [privacy checklist for phone and e-mail checks](/blog/privacy-checklist-for-phone-and-email-checks).

## How do you calibrate thresholds without hurting real users?

Guessed weights become good weights through measurement.

1. **Run in shadow mode first.** Compute the score on live traffic for two to four weeks without acting on it. Record which accounts later turned out to be abusive and which were fine.
2. **Keep a holdout.** Let a small random share of step-up decisions pass as "allow" and watch what they do. Without a holdout you can't measure how many good users your rules stop, because stepped-up users who give up never show you they were real.
3. **Measure per reason code**: how many flagged numbers were confirmed bad, and how many were good. Drop or shrink signals that mostly catch good users.
4. **Set thresholds by country.** Signals mean different things in different markets. In countries where VoIP and app-based numbers are common, a `voip` answer is weaker evidence. In countries where mobile and fixed ranges overlap, line type is less decisive; our research on where you can't tell a mobile from a landline lists them. Reputation data is also denser in some regions than others, so `no_reports` means less where reports are sparse.
5. **Re-check monthly.** Attack patterns move. A threshold that was right in one quarter can be too loose or too strict in the next.

## How do you stay fair to legitimate users?

Some real customers trip phone signals more often: people using app-based numbers for privacy, travellers, and people who recently switched carrier. Our guide to app-based numbers at sign-up explains why detecting them is useful and blocking them outright usually isn't.

Practical guard rails:

- Give every step-up a path a real person can complete, such as another verification channel or a short review.
- Never block on one weak signal.
- Don't use phone signals for eligibility decisions like credit, housing or employment. Our [acceptable use policy](/legal/acceptable-use) forbids it.
- Never try to infer who the person is. MobileValidate returns no names, profiles or demographics, and the score shouldn't reconstruct them.

## What does GDPR say about automated decisions?

Article 22 of the GDPR gives people "the right not to be subject to a decision based solely on automated processing, including profiling", where that decision produces legal effects or "similarly significantly affects" them [1]. There are exceptions, for example where the decision is necessary for a contract or based on explicit consent. In those cases the controller must still provide safeguards, "at least the right to obtain human intervention", to express a point of view and to contest the decision [1].

Whether a blocked sign-up counts as "similarly significant" depends on the service and the context. A three-outcome design with a human review path and reason codes puts you in a better position either way. This is not legal advice: check your own case with counsel, and see [is a phone number personal data under GDPR?](/blog/is-a-phone-number-personal-data-gdpr) for the basics.

## What does the score cost to run?

Run the cheap checks everywhere and the expensive ones only where they change a decision. At current prices on the [pricing page](/pricing), a real-time carrier lookup costs $0.002, an MNP lookup $0.001 and an HLR lookup $0.005 per conclusive answer. Spam reputation, where enabled, is $0.002 in real time. Repeat checks of the same number inside the freshness window come from your account's cache and are free.

A common pattern is carrier lookup on every sign-up, HLR only before sending a code or on step-up, and reputation for high-value flows such as payouts. All of them fit in one `POST /v1/lookup` request; see the [lookups documentation](/docs/lookups).

## What are the key takeaways?

- A phone number risk score exists to choose between allow, step up and block. Define the flow and its actions before the weights.
- Use hard rules for clear negatives and a small points table for combinations of weak signals. Add a model only once you have labelled outcomes.
- Unknown results add zero points. They aren't charged and they say nothing about the user.
- Attach reason codes to every decision for support, appeals and tuning.
- Calibrate in shadow mode, keep a holdout, and set thresholds per country.
- Keep a human review path, never block on one weak signal, and remember Article 22 GDPR for solely automated decisions with significant effects.

For the flow-level view, see the [OTP and sign-up fraud](/use-cases/otp-and-signup-fraud) and [account security](/use-cases/account-security) use cases.

## Sources

1. [Regulation (EU) 2016/679 (General Data Protection Regulation), Article 22: Automated individual decision-making, including profiling](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng) — Official Journal of the European Union (EUR-Lex), 2016

## Frequently asked questions

### What is a phone number risk score?

It is a number, or a small set of levels, that sums up how much extra caution a phone number deserves in one flow, such as a sign-up or a payout. You build it from facts about the number (line type, network status, porting, reputation) plus your own signals (velocity, IP country), and map it to allow, step up or block.

### Should an unknown lookup result raise the risk score?

No. Unknown means the check gave no conclusive answer, for example because of missing data or a timeout. It adds zero points, it is not charged, and the flow should behave as if that check had not run.

### Is a VoIP number a reason to block a sign-up?

Not on its own. Many real customers use app-based or VoIP numbers. Treat voip as a modest weight that can trigger a step-up, such as another verification method, when it appears together with other signals.

### Do I need machine learning to score phone numbers?

Usually not at first. Start with a few hard rules and a transparent points table. A model only helps once you have enough labelled outcomes, such as confirmed fraud and confirmed good users, to train and test it.

### Does GDPR allow automated blocking based on a phone risk score?

Article 22 GDPR gives people the right not to be subject to decisions based solely on automated processing that have legal or similarly significant effects, with exceptions and safeguards. Keep a human review path and a way to contest decisions. This is not legal advice; check your own case with counsel.
