← Blog

How to detect VoIP phone numbers

Line-type metadata flags numbers that sit in VoIP-designated ranges — useful before you dial or text, and honest about the hosted and ported VoIP it cannot see.

Contents

A VoIP number looks like any other number right up until you try to work it. It might refuse SMS, route voice to a softphone that never rings, or be the cheap, disposable line a fake signup reaches for. So before your reps dial or your platform texts, you want the VoIP rows in your list flagged. The good news: a lot of VoIP is detectable from number metadata alone, for free, before any live lookup runs. The honest news, which is most of this post: metadata catches the VoIP that lives in ranges designated for VoIP — not the VoIP that borrows an ordinary mobile or fixed-line range. Here is how to detect VoIP phone numbers with line-type metadata, and exactly where that detection stops.

Why flag VoIP before you dial

A VoIP line does not behave like the landline or mobile next to it in your file:

  • VoIP has no fixed geography. It does not map to a physical location or a single carrier the way a geographic landline does.
  • Delivery is unreliable. Some VoIP lines silently drop inbound SMS or forward voice into an app.
  • Disposable VoIP is a signup-fraud favorite — a number that sails past a naive "is this even a number" check.

No percentages here — the benchmark has not published, and you do not need invented stats to know a landline and a burner softphone deserve different handling. Worth stating plainly: flagging VoIP is a segmentation and hygiene step, not a compliance step. It does not scrub the Do-Not-Call registry, and it is not legal advice — for DNC or TCPA obligations you use the official registries (such as the US National Do Not Call Registry) and your own counsel. Boundstone's job is the hygiene layer: drop the invalid, segment by line type, normalize to E.164.

Detect VoIP with line_type

The line_type field on a phone response comes from Google's libphonenumber metadata — the type a number's range is designated for in its national numbering plan. When a range is set aside for VoIP, libphonenumber reports it, and Boundstone returns line_type: "voip".

curl -X POST https://api.boundstone.io/v1/verify/phone \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+16504472983"}'

The field to read is line_type:

{
  "valid": true,
  "e164": "+16504472983",
  "country": "US",
  "line_type": "fixed_line_or_mobile",
  "national_format": "(650) 447-2983",
  "checks": {
    "performed": ["format", "region", "line_type_metadata"],
    "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
  }
}

That number sits on a range North America shares between fixed line and mobile, so the plan cannot split it — more on fixed_line_or_mobile below. When a number instead sits in a range its numbering plan reserves for VoIP, the same call returns the identical shape with one field changed: line_type reads voip. That is the row to flag. You can try a single number with no signup on the keyless phone validator tool, and the field-by-field breakdown of every type value lives in what is phone line type.

Flag VoIP across a whole list

One number is a curl call; a lead file is a bulk job. POST /v1/bulk/phone takes a raw CSV body — one column of numbers — and returns a job you poll.

curl -X POST https://api.boundstone.io/v1/bulk/phone \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @leads.csv

You get back HTTP 202 and a job_id. One credit is reserved per row and refunded for any row that errors, so a messy file does not quietly cost you. Free jobs cap at 250 rows; paid plans go to 10,000. Pull the results when the job finishes and filter on line_type:

curl https://api.boundstone.io/v1/bulk/JOB_ID/results.csv \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -o results.csv
import csv

voip, keep = [], []
with open("results.csv", newline="") as f:
    for row in csv.DictReader(f):
        if row["valid"] != "true":
            continue                        # not a real number — drop it
        if row["line_type"] == "voip":
            voip.append(row)                # designated VoIP — flag or hold
        else:
            keep.append(row)                # mobile, fixed_line, and the rest

print(f"flagged {len(voip)} VoIP, kept {len(keep)}")

That gives you a VoIP bucket to suppress or hold for review, and a cleaner main list — though metadata still cannot promise any kept number will ring. The single-number, in-process version, without the CSV round-trip, is in validate a phone number in Python. Prefer no code at all? The dashboard runs the same job: upload a CSV or paste numbers one per line and download the results, same 250-row free cap and per-row accounting.

The ceiling: designated VoIP is not all VoIP

Here is the honest boundary, and it is the reason to trust the flag you do get. line_type reports the type a number's range is designated for. It catches VoIP that lives in ranges the numbering plan set aside for VoIP. It does not catch VoIP that runs on top of a range designated for mobile or fixed line. Two common ways a VoIP number hides from metadata:

  • Hosted VoIP on a normal range. A provider hands you a number from an ordinary mobile or geographic block and runs it over VoIP. The range still reads "mobile," so line_type reads mobile.
  • Ported numbers. A number ported from a mobile carrier onto a VoIP service keeps its original range's designation. Metadata reflects where a number was born, not where it lives now.

To see through either, you need a live carrier lookup — an HLR or LRN query that asks the network who currently serves the number. Boundstone does not run that by default, and it says so in every response:

"checks": {
  "performed": ["format", "region", "line_type_metadata"],
  "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
}

carrier_lookup, ported_status, and hlr_liveness are all not_performed. That live layer is a paid opt-in (hlr:true, 5 credits, refunded on abstain), so this post claims no detection rate for the metadata pass — only that it catches designated-VoIP ranges, which is a real, useful subset, not the whole set.

One more field to read honestly: fixed_line_or_mobile. In North America especially, many ranges are shared between fixed line and mobile, so libphonenumber cannot tell them apart and returns fixed_line_or_mobile. That value is the API declining to guess — treat it as "unknown," not as "not VoIP." A VoIP number reusing such a range would land there too. And valid: true never means "live" — it means well-formed and correctly ranged, nothing about whether a phone will ring.

The short version

  • Read line_type. voip means the number sits in a range designated for VoIP — flag it before you dial or text.
  • Bulk-bucket a whole file. Run it through POST /v1/bulk/phone, then bucket by line_type from results.csv. One credit per row, refunded on any row that errors.
  • Know the ceiling. Metadata catches designated VoIP, not hosted or ported VoIP that reuses a mobile or fixed-line range. Those need a live carrier lookup — carrier_lookup, ported_status and hlr_liveness are not_performed unless you send hlr:true.
  • Treat fixed_line_or_mobile as "unknown," not "not VoIP" — it is the API refusing to guess on a shared range.
  • The free tier is 250 credits a month, no card, and credits never expire — enough to flag VoIP across a real list before you commit.

Frequently asked questions

How do you detect if a phone number is VoIP?

The practical method is line-type metadata: a validation lookup takes a well-formed number in E.164 format, maps it to its assigned number range, and returns a line-type classification such as mobile, fixed line, or VoIP. Boundstone's phone check performs format, region, and line-type metadata, so it flags numbers that fall in designated VoIP ranges. It does this without a carrier lookup or HLR query, which makes it a fast, low-cost signal rather than a real-time network dip. Note that this reflects how the number range is registered, not which provider the number happens to route through at this exact moment.

Is VoIP number detection always accurate?

No detection method is perfect. Line-type metadata reliably catches numbers that sit in ranges designated as VoIP, but number porting can move a number between line types after it was first assigned, and a metadata lookup does not observe that change. Boundstone is explicit about this in its honesty contract: a phone check performs format, region, and line-type metadata, while carrier lookup, ported status, and HLR liveness are listed as not performed. Treat a VoIP flag as a strong hint about the number range, not a guarantee about the number's current carrier.

If a number is flagged as VoIP, does that mean it is fake or unusable?

Not at all, since many legitimate people and businesses use VoIP numbers, so a VoIP flag is a risk signal to weigh rather than proof that a number is fake, unreachable, or spam. It also helps to know exactly what a valid result proves: confirming a number is well-formed and returning its line type is hygiene, it does not confirm the line is live or in service, and it is not a Do-Not-Call or TCPA compliance check. Boundstone does not perform liveness or carrier checks by default (both are a paid opt-in via hlr:true) and does not scrub numbers against DNC lists, so use VoIP metadata to prioritize and score, then apply your own compliance and contactability rules.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist