# How to validate leads before they hit your CRM

_2026-07-18 · Boundstone (https://boundstone.io/blog/validate-leads-before-crm)_


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:

```csv
phone
+16504472983
650-447-2983
5551234
+442071838750
```

Post it:

```bash
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:

```bash
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:

```json
{
  "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:

```json
{
  "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

1. Export the raw list to CSV, one column per file.
2. POST each column to `/v1/bulk/phone` and `/v1/bulk/email` — or upload in the dashboard.
3. Pull `results.csv` for each job.
4. Drop `valid: false` phones and `valid_syntax: false` / `disposable: true` / `mx_found: false` emails.
5. Write `e164` back as the canonical phone field.
6. Tag `line_type` and `role_account` so your routing rules can read them.
7. Import the survivors.

Going deeper on either side: [bulk email validation over CSV](/blog/bulk-email-validation-csv) and [cleaning a phone list before calling](/blog/clean-phone-list-before-calling). For a one-off address, the [free email validator](/tools/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/phone` and `/v1/bulk/email`; one credit per row, refunded on error.
- **Drop, normalize, segment.** Drop the invalid and impossible, normalize phones to `e164`, segment by `line_type` and `role_account`.
- **`checks.not_performed` is 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.
