SMS pumping and toll fraud: what the numbering plans can stop
SMS pumping turns a signup or login form into income for someone else. A script asks for one-time codes for thousands of numbers in ranges whose operator, or a party on the route, shares the fee for delivering the message with the attacker. You pay for every message, and no one ever types a code. Toll fraud, or international revenue share fraud, is the same with calls: a verification call to a premium-rate or high-cost range earns its holder money.
What the plans let you limit
- A text goes only to a mobile range. Most fixed lines cannot receive a text, and a premium-rate, shared-cost, personal-number or UAN range has no reason to receive a code. The +1 plans cannot tell mobile from fixed line, so their numbers stay in.
- Only to the countries you serve. A service with users in one country has no reason to text the other 244 plans. The country comes from the number's plan, not from the address the request came from.
- One counter per block of numbers. Pumping walks through a range: …123456, …123457, …123458. Count the codes sent per block of 10,000 numbers, the national number without its last four digits, and slow down a block that suddenly asks for many. In +1 that block is the central office code, the first six digits.
- No verification call to the costly ranges. 150 of the 245 plans have premium-rate ranges, 53 shared-cost, 59 personal-number and 63 UAN ranges. The line type from
parsenames them before the call is placed.
The code
Two additions to the guides' file: mayTextTo for the first two limits, rangeKey for the counter's key; the counter itself lives wherever your service counts things. A service in the United Kingdom, with the United Kingdom, the United States and France loaded:
| Number | Line type | Code sent | Counter key |
|---|---|---|---|
| +44 7400 123456 | mobile | yes | +44 740012 |
| +44 7400 123457 | mobile | yes | +44 740012 |
| +44 7400 123458 | mobile | yes | +44 740012 |
| +44 70 1234 5678 | personalNumber | no | +44 701234 |
| +44 901 234 5678 | premiumRate | no | +44 901234 |
| +44 121 234 5678 | fixedLine | no | +44 121234 |
| +33 6 12 34 56 78 | mobile | no | +33 61234 |
| +1 201-555-0123 | fixedLineOrMobile | no | +1 201555 |
The three numbers in a row share one key: three requests count against one block.
Add it at the end of phone.mjs, the file from the JavaScript guide:
// A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB).
export const mayTextTo = (n, countries) =>
(n.type === 'mobile' || n.type === 'fixedLineOrMobile') && countries.includes(n.region)
// One counter per block of 10,000 numbers: the country code and the national number without its last four digits.
export const rangeKey = (n) => `+${n.cc} ${n.nsn.slice(0, -4)}`Add it at the end of phone.py, the file from the Python guide:
def may_text_to(number: PhoneNumber, countries: set[str]) -> bool:
"""A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB)."""
return number.type in ("mobile", "fixedLineOrMobile") and number.region in countries
def range_key(number: PhoneNumber) -> str:
"""One counter per block of 10,000 numbers: the country code and the national number without its last four digits."""
return f"+{number.cc} {number.nsn[:-4]}"Add it inside the class in Phone.java, the file from the Java guide:
/** A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB). */
public static boolean mayTextTo(PhoneNumber n, List<String> countries) {
return (n.type().equals("mobile") || n.type().equals("fixedLineOrMobile")) && countries.contains(n.region());
}
/** One counter per block of 10,000 numbers: the country code and the national number without its last four digits. */
public static String rangeKey(PhoneNumber n) {
return "+" + n.cc() + " " + n.nsn().substring(0, Math.max(0, n.nsn().length() - 4));
}Add it at the end of phone.go, the file from the Go guide:
// MayTextTo reports whether a one-time code may go to n: a mobile range in one of countries (ISO codes such as "GB").
func MayTextTo(n Number, countries []string) bool {
return (n.Type == "mobile" || n.Type == "fixedLineOrMobile") && slices.Contains(countries, n.Region)
}
// RangeKey is one counter per block of 10,000 numbers: the country code and the national number without its
// last four digits.
func RangeKey(n Number) string { return "+" + n.CC + " " + n.NSN[:max(0, len(n.NSN)-4)] }Add it inside the class in Phone.php, the file from the PHP guide:
/** A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB). */
public static function mayTextTo(array $number, array $countries): bool
{
return in_array($number['type'], ['mobile', 'fixedLineOrMobile'], true) && in_array($number['region'], $countries, true);
}
/** One counter per block of 10,000 numbers: the country code and the national number without its last four digits. */
public static function rangeKey(array $number): string
{
return '+' . $number['cc'] . ' ' . substr($number['nsn'], 0, -4);
}Add it inside the module in phone.rb, the file from the Ruby guide:
# A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB).
def self.may_text_to?(number, countries)
%w[mobile fixedLineOrMobile].include?(number.type) && countries.include?(number.region)
end
# One counter per block of 10,000 numbers: the country code and the national number without its last four digits.
def self.range_key(number)
"+#{number.cc} #{number.nsn[0...-4]}"
endAdd it inside the class in Phone.cs, the file from the C# guide:
/// <summary>A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB).</summary>
public static bool MayTextTo(PhoneNumber n, IEnumerable<string> countries) =>
(n.Type == "mobile" || n.Type == "fixedLineOrMobile") && countries.Contains(n.Region);
/// <summary>One counter per block of 10,000 numbers: the country code and the national number without its last four digits.</summary>
public static string RangeKey(PhoneNumber n) => $"+{n.Cc} {n.Nsn[..Math.Max(0, n.Nsn.Length - 4)]}";Add it in src/phone.rs, before the tests, the file from the Rust guide:
/// A one-time code goes only to a mobile range in a country you serve (ISO codes such as GB).
pub fn may_text_to(n: &PhoneNumber, countries: &[&str]) -> bool {
matches!(n.kind, "mobile" | "fixedLineOrMobile") && countries.contains(&n.region)
}
/// One counter per block of 10,000 numbers: the country code and the national number without its last four digits.
pub fn range_key(n: &PhoneNumber) -> String {
format!("+{} {}", n.cc, &n.nsn[..n.nsn.len().saturating_sub(4)])
}Sources: National numbering plans, ITU-T E.164 (ITU); National Telephone Numbering Plan (Ofcom). Plans as published on 2026-10-25.