← Blog

How to clean a phone number list before an outbound campaign

Bulk-validate the file, drop the numbers that cannot be real, and route the rest by line type — with a clear line on what validation does and does not tell you.

Contents

You have a phone number list — a CRM export, a purchased lead file, a form dump — and before your reps start dialing, you want to clean it. To clean a phone number list means two concrete things: remove the rows that cannot be a real number, and sort the survivors by the kind of line they are, so you route SMS and voice correctly. Here is how to do both over one API call, and — just as important — where the cleaning stops.

Do the free, local part first

Before an API touches your file, know what your own stack already does. Google's libphonenumber — the library behind most phone validation, with ports in Python (phonenumbers), JavaScript (libphonenumber-js), Java, Go, and more — parses and validates number format and region entirely offline. If all you need is to drop obviously malformed rows, that runs on your own machine for free. The in-process walkthrough lives in validate a phone number in Python, and you can spot-check a single number with the keyless phone validator tool — no signup.

So why call an API at all? Two reasons. One contract: instead of maintaining a different libphonenumber build and metadata version in every service, you get one consistent response shape across every language your team writes in. And bulk: one request cleans the whole file, with per-row credit accounting, instead of you scripting the loop and rate-limiting yourself.

Bulk-validate the list

POST /v1/bulk/phone takes a raw CSV body — one column of phone numbers — and returns a job you poll for results.

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. Credits are reserved per row at submit and refunded for any row that errors, so a broken file does not quietly cost you. The free tier caps a job at 250 rows; paid plans go to 10,000. When the job finishes, pull the results:

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

Each row carries the same fields as the single POST /v1/verify/phone endpoint: valid, e164, country, line_type, and national_format.

Filter results.csv by line_type

Now the actual cleaning. Two passes: drop what cannot be valid, then segment what remains by line_type.

import csv

keep, drop = [], []
with open("results.csv", newline="") as f:
    for row in csv.DictReader(f):
        if row["valid"] != "true":
            drop.append(row)                  # impossible / unparseable — never dial
        elif row["line_type"] == "mobile":
            row["channel"] = "sms_or_call"
            keep.append(row)
        elif row["line_type"] == "fixed_line":
            row["channel"] = "call_only"       # don't text a landline
            keep.append(row)
        else:
            row["channel"] = "call_only"       # voip, toll_free, or ambiguous — call, verify by ear
            keep.append(row)

print(f"kept {len(keep)}, dropped {len(drop)}")

e164 gives you a dialer-ready number; line_type lets you skip texting landlines and hold ambiguous lines for a live call. That is a cleaner, better-routed list than the file you started with.

What this does not tell you

Here is the honest boundary, and it is the whole point of validating with Boundstone. A phone response tells you exactly what it checked:

{
  "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"]
  }
}

valid: true means the number is well-formed and correctly ranged for its country. It does not mean the number is in service, currently assigned, or that a phone will ring. Boundstone does not perform carrier lookup, ported-status, or HLR liveness by default — you can see them listed plainly in not_performed. Send hlr:true and the dip runs for 5 credits, refunded when the network cannot answer.

line_type is metadata-derived too. When a number's range is ambiguous, you get fixed_line_or_mobile — the API telling you it cannot distinguish, rather than guessing. Treat line_type as a strong hint for routing, not a guarantee, especially for numbers that may have ported between carriers.

No code required

If you would rather not script it, the dashboard takes the same job. Upload a CSV or paste numbers one per line, run the batch, and download the results CSV — same 250-row free cap, same per-row credit accounting. That is good for a one-off list before a campaign; the API is for when cleaning becomes a step in your pipeline. For the wider workflow this fits into, see outbound sales.

The short version

  • Drop every row where valid is not true — those cannot be dialed. That pass alone removes the junk.
  • Segment the rest by line_type: mobile can take SMS, fixed_line should be call-only, and fixed_line_or_mobile is the API admitting it does not know.
  • Understand the ceiling. Valid means well-formed and correctly ranged, not "reachable." Carrier lookup, ported-status and HLR liveness are not_performed unless you send hlr:true.
  • The free tier is 250 credits a month, no card, and credits never expire — enough to clean a small list or trial the bulk flow before you commit.

Frequently asked questions

How do I clean a phone number list before an outbound campaign?

Start by normalizing every number to E.164 format and checking each one against its region's numbering rules, then drop entries that are malformed or impossible. From there, filter by line type using the returned metadata and deduplicate the file so you are not dialing the same contact twice. Boundstone runs the format, region, and line-type metadata checks with no cost on the free tier of 250 credits a month, no card required and credits that never expire, and its bulk CSV endpoint reserves one credit per row and refunds any row that errors. Keep in mind this confirms a number is well-formed, not that it is currently live or reachable.

What does a 'valid' phone number actually prove?

A valid result from Boundstone means the number is correctly formatted and possible for its region, and it returns line-type metadata such as mobile or fixed line alongside the E.164 and national formats. It does not prove the number is currently assigned, still in service, or that a call will connect. Confirming a line is live requires a carrier lookup or HLR query, which Boundstone offers as a paid opt-in (hlr:true) and marks not_performed by default. Every response lists exactly what was tested under checks.performed and what was not under checks.not_performed, so you never mistake a format pass for a liveness guarantee.

Can phone validation tell me which numbers are disconnected or ported before I dial?

No. Format and line-type validation cannot tell you whether a number has been disconnected or ported to a different carrier, because that information only comes from a live carrier or HLR lookup — which Boundstone runs as a paid opt-in (hlr:true) and marks as not_performed by default. What it does do cheaply is remove numbers that are malformed, impossible for their region, or the wrong line type for your campaign, which strips a lot of dead weight before you spend on dialing. If a vendor claims that plain validation proves a number is active, treat that as a red flag.

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