VoIP, Burner or Real? How Developers Catch Fake Phone Numbers at Signup
Your signup form does not have a bot problem. It has a phone number problem. Promo abusers sign up five times with five virtual numbers. SMS pumping rings feed you premium-rate numbers and collect a cut of every OTP you send. And perfectly honest users typo their own numbers, then wonder why the verification code never arrives. All three failure modes walk through the same door, and all three are catchable in the two hundred milliseconds between “user hits submit” and “you send anything.” The tool for the job is a carrier lookup: asking the numbering system itself what kind of line you are about to trust.
Why Signup Is the Only Cheap Place to Catch This
Every stage after signup makes a bad number more expensive. At the form, a rejected number costs you one API call that rounds to nothing. One step later, you have paid for an SMS that either vanished into a dead line or, worse, paid a fraud ring through an artificially inflated traffic scheme. A month later, that fake account has claimed a referral bonus, left fake reviews, or become one of the thousand ghosts inflating your MAU chart before a board meeting.
SMS pumping alone has become a line item worth engineering against. The scheme is simple: fraudsters trigger OTP sends to number ranges they profit from, your telecom bill grows, and nothing else happens. The defense is equally simple: know what kind of number you are texting before you text it.
What a Carrier Lookup Actually Tells You
A phone number lookup api takes a phone number and returns what the global numbering plan knows about it: whether the number is validly structured and assigned, which country and carrier it belongs to, and the line type. That last field is the fraud signal. A response from the numverify API looks like this:
{
“valid”: true,
“number”: “14158586273”,
“international_format”: “+14158586273”,
“country_code”: “US”,
“country_name”: “United States of America”,
“location”: “Novato”,
“carrier”: “AT&T Mobility LLC”,
“line_type”: “mobile”
}
Four fields carry the decision:
valid kills typos and junk instantly. If the number cannot exist under its country’s numbering rules, nothing downstream matters.
line_type separates mobile from landline from VoIP. Mobile is the normal case. Landline means your SMS will never arrive, so route that user to voice verification instead of letting them wait for a text. VoIP is where the burner apps, virtual number services and disposable-number sites live, and it deserves different treatment than a flat yes.
carrier adds texture. A number on a major mobile network reads differently than one on a wholesale VoIP provider you have seen in your abuse logs before.
country_code lets you compare the number’s home against the user’s IP geolocation and card country. Any single mismatch is weak evidence. All three pointing in different directions is a pattern.
The honest limitation: a lookup proves the number is real and tells you its type. It does not prove the person typing it owns it. Ownership is what the OTP is for. The lookup’s job is making sure the OTP is worth sending.
The Decision Tree That Pays for Itself
Teams overthink this. In practice four rules cover nearly everything:
- Invalid number: reject at the form. Show which part looks wrong (“US numbers have 10 digits”) so honest users can fix a typo in five seconds.
- Landline: skip SMS. Offer a voice call OTP or email verification. You just rescued a real user your old flow would have silently lost.
- Mobile: proceed. Send the OTP, store the number in E.164 format, move on. This is 85 to 95 percent of signups for most consumer products.
- VoIP: decide per product, not per prejudice. VoIP is not a crime; plenty of real users run Google Voice. The right move depends on what the account can do. Free trial with promo credits: hold the credits until a stronger check passes. Marketplace with payouts: require a non-VoIP number or add manual review. Content app with nothing to steal: let it through and tag the account for monitoring.
The point of the tree is that “block all VoIP” and “ignore line type entirely” are both wrong, and the data lets you be precise instead.
Wiring It In: Code and the Three Mistakes to Avoid
The integration is one GET request server-side. Here is a compact version with the decision logic included:
import os, requests
API_KEY = os.environ[“NUMVERIFY_KEY”]
def check_number(number: str, country: str = “US”) -> dict:
try:
r = requests.get(
“https://apilayer.net/api/validate”,
params={“access_key”: API_KEY, “number”: number,
“country_code”: country},
timeout=5,
)
r.raise_for_status()
d = r.json()
except requests.RequestException:
return {“action”: “allow”, “reason”: “lookup_unavailable”}
if d.get(“success”) is False:
return {“action”: “allow”, “reason”: “lookup_error”}
if not d.get(“valid”):
return {“action”: “reject”, “reason”: “invalid_number”}
line = d.get(“line_type”) or “unknown”
if line == “landline”:
return {“action”: “voice_otp”, “e164”: d[“international_format”]}
if line == “voip”:
return {“action”: “step_up”, “e164”: d[“international_format”],
“carrier”: d.get(“carrier”)}
return {“action”: “sms_otp”, “e164”: d[“international_format”]}
Three deliberate choices here, each learned the hard way somewhere:
Fail open, not closed. If the lookup API is unreachable, the function allows the signup rather than blocking it. A provider hiccup should never lock real users out of your product; queue a recheck instead.
Validate server-side only. A client-side check is user experience. The server-side check is the one fraud cannot bypass, and it is also where your API key stays secret.
Store E.164, always. One canonical format (+14158586273) across your database ends the duplicate-account trick of signing up with the same number formatted three ways.
The mistakes to avoid are the mirror images: do not block on API downtime, do not trust a check the browser performed, and do not store numbers in whatever format users typed.
What Changes After You Ship It
The numbers from teams who add this are consistent in shape. OTP spend drops immediately, because invalid and landline numbers stop receiving SMS attempts. Verification completion rates rise, which feels paradoxical until you realize how many “verification failed” complaints were typos the form could have caught. Duplicate and promo-abuse accounts fall, since disposable numbers now cost the fraudster a step-up they rarely bother with. And your support queue loses its steady trickle of “I never got the code” tickets.
Bulk cleanup compounds it. Running an existing contact database through phone number validity checks before a campaign tells you which contacts are reachable at all, so your sales team stops dialing numbers that were never real and your SMS campaigns stop paying for ghosts.
For higher-risk flows, pair line type with a reputation check: a fraud phone number lookup adds the scam and spam context around a number, which turns a suspicious signup from a hunch into a documented decision.
Start with One Endpoint
You do not need a fraud team to do any of this. One endpoint, one key, forty lines of handler code, and your signup form stops being the cheapest attack surface in your product. The numverify phone number validation api has a free tier that covers a real pilot: wire it into staging this week, run your last thousand signups through it, and count how many VoIP and invalid numbers were already inside. That number is usually the whole business case.