How to validate leads before they hit your CRM
Run every lead through a bulk email and phone check before import, so the CRM only ever meets normalized, segmented rows — and never the provably-bad ones.
Contents
Every bad row you import is a row you pay for later — a sequence that bounces, a dial that dead-ends, a rep's afternoon spent on a number that was never a number. The cheapest place to catch junk is before it lands in the CRM, while it's still an anonymous line in a CSV and not a "contact" with a lifecycle stage and an owner. This is a hygiene step, not a magic wand. You're removing the provably-bad, not the merely-stale. Here's how to run email and phone through a bulk check at import, and exactly where the honesty line sits.
Decide what "bad" means before you import
Junk comes in a few flavors, and they are not equal:
- Syntax-broken emails (
john@,jane at company dot com) — provably bad. - Disposable-domain emails (
@mailinator.com) — provably low value. - Role accounts (
info@,sales@) — not a person; segment or drop. - Impossible phone numbers — wrong length, no such area code — provably bad.
- Valid numbers on the wrong line type — segment, do not SMS a fixed line.
Boundstone's job is the first gate: drop the provably-bad, normalize the survivors, tag them so your CRM starts clean. It does not tell you whether a well-formed inbox actually receives mail, or whether a valid number is switched on right now. Hold that thought — it's the whole point of the honesty contract below.
Run the list through bulk, before it touches the CRM
Bulk takes a raw CSV and hands back a job id. One column, one value per line:
phone
+16504472983
650-447-2983
5551234
+442071838750
Post it:
curl -X POST https://api.boundstone.io/v1/bulk/phone \
-H "Authorization: Bearer bs_live_YOUR_KEY" \
-H "Content-Type: text/csv" \
--data-binary @phones.csv
You get back HTTP 202 and a job_id. Pull the results when it finishes:
curl https://api.boundstone.io/v1/bulk/JOB_ID/results.csv \
-H "Authorization: Bearer bs_live_YOUR_KEY"
Email is the same call at /v1/bulk/email. Free tier runs 250 rows per job, paid runs 10k. One credit is reserved per row and refunded for any row that errors — that's a row the system couldn't process, not a row that comes back invalid. Learning a number is junk is a successful check, so it costs a credit; a row that failed to process does not. And if you'd rather not touch a terminal, the dashboard has a CSV upload and paste-box that runs the identical job.
Read the fields, then drop, normalize, or segment
A single-row phone response shows you what each column means:
{
"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: false drops the row. Write e164 back as your canonical phone field — store +16504472983, not (650) 447-2983, so dedupe and dialers agree. Segment on line_type. When it comes back FIXED_LINE_OR_MOBILE, the numbering plan genuinely can't distinguish the two — common across North America — so don't guess a channel; route it somewhere that tolerates either, or fall back to a call.
Email works the same way:
{
"valid_syntax": true,
"domain": "example.com",
"mx_found": true,
"disposable": false,
"role_account": false,
"free_provider": false,
"checks": {
"performed": ["syntax", "mx", "disposable_list", "role_list"],
"not_performed": ["smtp_mailbox", "catch_all"]
}
}
Drop on valid_syntax: false, disposable: true, or mx_found: false — a domain with no MX can't receive mail at all. Segment role_account: true away from personal outreach. free_provider is a scoring flag, not a reason to drop; a Gmail address is still a person.
The line between provably-bad and merely-stale
checks.not_performed is not a footnote. It's the contract, and it's what keeps this honest.
For phone, not_performed is carrier_lookup, ported_status, hlr_liveness. So valid: true means well-formed and possible for its region with a designated line type. It does not mean the number is in service, reachable, or answered. A long-disconnected mobile still validates. And line_type is libphonenumber metadata — the number's designated type in its numbering plan, not a live network check — so a VoIP line ported onto a mobile range still reads as mobile. To know a line is live and who carries it today, you need a live carrier/HLR lookup — a paid opt-in via hlr:true at 5 credits, refunded when the network cannot answer.
For email, not_performed is smtp_mailbox and catch_all. Syntax is clean and the domain publishes MX, so mail can route there — but Boundstone doesn't knock on the mailbox or detect a catch-all. A perfectly formed address at a real domain can still bounce.
One more line worth stating plainly: this is data validation, not a Do-Not-Call scrub and not legal advice. Dropping impossible numbers is hygiene. TCPA and DNC obligations run through the official registries (the US National Do Not Call Registry, state lists) and your counsel, not through this API.
Wire it into the import
- Export the raw list to CSV, one column per file.
- POST each column to
/v1/bulk/phoneand/v1/bulk/email— or upload in the dashboard. - Pull
results.csvfor each job. - Drop
valid: falsephones andvalid_syntax: false/disposable: true/mx_found: falseemails. - Write
e164back as the canonical phone field. - Tag
line_typeandrole_accountso your routing rules can read them. - Import the survivors.
Going deeper on either side: bulk email validation over CSV and cleaning a phone list before calling. For a one-off address, the free email validator runs the same syntax, MX, disposable, and role checks in the browser.
The short version
- Validate at import, not after. The CRM should never meet a provably-bad row.
- Bulk-POST your CSVs to
/v1/bulk/phoneand/v1/bulk/email; one credit per row, refunded on error. - Drop, normalize, segment. Drop the invalid and impossible, normalize phones to
e164, segment byline_typeandrole_account. checks.not_performedis honest: no mailbox knock (smtp_mailbox,catch_all), and no live-line check by default (carrier_lookup,ported_status,hlr_liveness). You remove the provably-bad, not the merely-stale.- Not a DNC scrub and not legal advice — that is the registries and your counsel.