Skip to content

How to clean a lead list before sequencing

· 3 min read · Email validation

To clean a lead list before sequencing, export it as CSV, dedupe on lowercased email, remove addresses you've already suppressed, verify every remaining row, and import only valid contacts into the main sequence. Put risky and unknown contacts in a separate, slower sequence or hold them. This takes minutes and protects the sending domain your whole team relies on.

It applies to any sequencer that imports contacts from a CSV or a CRM, whatever the brand. Sequencers send what you give them. Cleaning the list is your job.

Step 1: Export and normalize

Export the list with at least email, first name, company and the record ID from your data source or CRM. Then:

  • Lowercase and trim the email column.
  • Drop rows with no email.
  • Dedupe on email. Lists built from several searches overlap more than you expect.
  • Dedupe across sequences too: if someone is already in an active sequence, don't enroll them twice.

Step 2: Remove what you already know is bad

Before paying for any verification, take out:

  • Your suppression list: previous hard bounces, unsubscribes and "not interested" replies. This is a legal and reputational issue, not just a cost one.
  • Customers and open opportunities, unless the sequence is meant for them.
  • System addresses like noreply@ and postmaster@. Nobody reads them.

Step 3: Verify every address

Data providers often label emails as verified. That label describes a check made at some point before your export. People change jobs, and the US Bureau of Labor Statistics puts median tenure with the current employer at 4.1 years (January 2026), so any list contains people who have moved on. Re-verify right before import.

Two ways to do it with pingutil:

Dashboard CSV upload. Upload the file (50 rows per upload) and download it with the result columns added. No code.

Batch API. For longer lists, loop over the file 50 addresses at a time. This Python script reads leads.csv and writes leads_verified.csv:

# pip install requests
import csv, os, time, requests

API = "https://pingutil.com/v1/verify/batch"
HEADERS = {"Authorization": f"Bearer {os.environ['PINGUTIL_API_KEY']}"}

with open("leads.csv", newline="") as f:
    rows = list(csv.DictReader(f))

out = []
for i in range(0, len(rows), 50):
    chunk = rows[i:i + 50]
    while True:
        r = requests.post(API, json={"emails": [row["email"] for row in chunk]},
                          headers=HEADERS, timeout=120)
        if r.status_code == 429:
            body = r.json()
            if body.get("error") == "quota_exceeded":
                raise SystemExit(f"Daily budget used; resume from row {i} after 00:00 UTC")
            time.sleep(body.get("retry_after_seconds", 60))
            continue
        r.raise_for_status()
        break
    for row, res in zip(chunk, r.json()["results"]):
        row.update(status=res["status"], reason=res["reason"],
                   score=res["score"], did_you_mean=res["did_you_mean"] or "")
        out.append(row)

with open("leads_verified.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=list(out[0].keys()))
    w.writeheader()
    w.writerows(out)

Accounts get 300 verifications a day, so a 1,000-row list takes four days at that rate; the script stops cleanly at the budget and tells you where to resume. Results are cached for 30 days, so re-running rows you've already checked is fast, though every row still counts toward the daily total.

Step 4: Split by result

Status Reason examples Where it goes
valid mailbox_exists Main sequence
invalid mailbox_not_found, no_mx, disposable_domain Remove. Add to suppression.
risky accept_all Separate low-volume sequence, after the main one runs clean
risky role_based Usually remove from outbound
unknown greylisted, blocked, timeout Re-verify in an hour or two, then decide

Catch-all (accept_all) is the hard case. The company's server accepts any address during the check, so a verifier can't tell you whether this person exists. pingutil reports these as risky, never valid. Catch-all email domains explains the mechanics, and role-based email addresses covers the info@ question.

If did_you_mean is filled in, the domain looks like a typo of a major provider. In a purchased list, nobody is there to confirm the correction, so drop the row rather than guess.

Step 5: Import and watch step one

Import valid contacts into the main sequence. After the first step sends, check the bounce rate before the risky sequence starts. Amazon SES, for comparison, recommends keeping hard bounces below 2%; if a verified list bounces more than that, something upstream is wrong and you should pause the source.

Feed every hard bounce from the sequencer back into your suppression list and CRM so the next export starts cleaner.

For the reasoning behind each step, read why verify before cold outreach. To see the result format before you script anything, paste a few addresses into the email verification demo.

Questions people ask

Do I need to verify emails from a data provider that says they are verified?
It is worth re-checking. A provider's verification happened at some point in the past, and work addresses go stale as people change jobs. Re-verifying right before import tells you what the mail servers say today.
Should risky contacts go into the same sequence?
No. Put them in a separate sequence with lower daily volume, or hold them until the main sequence has run cleanly. That keeps a batch of catch-all bounces from landing on the same days as your best prospects.
How many addresses can I verify at once with pingutil?
Up to 50 per batch API request or per dashboard CSV upload, within 60 requests a minute and 300 verifications a day per account.