How to Clean a 10,000-Row Email List via API and MCP at 1 Point per Address
Clean an email list via API or MCP: check each address for 1 point ($1 per 1,000), flag catch-all domains and keep bounces off your sender reputation.
By the NeuralVerge team · 13 min read · Updated November 2026
▸On this page
To clean an email list, check every address for deliverability before anyone sends to it, drop the clearly bad ones and treat the uncertain ones with care. Email validation does that check for 1 point per address, so a 10,000-row list costs 10,000 points. You reach it through a REST request or through MCP, and this post is the workflow around it: how to read the result, how to script a full list, and where an agent fits.
The short version
- —What you get: for each address, a verdict, an explanation, a catch-all flag, the mail provider behind the domain and a confidence rating.
- —Who it is for: anyone about to send to a list that is old, imported, scraped by a team member or assembled from several tools: marketing and sales ops, founders running outreach, teams moving a CRM.
- —What it costs: 1 point per address. At the rate every plan works out to, that is $1 per 1,000 and $10 for 10,000 addresses. The dollar figures here are derived from the plan prices, not quoted from the source page.
- —What it does not do: it checks deliverability. It does not tell you whether the person still works there, whether they agreed to be emailed, or whether a mailbox that exists today will still exist next quarter.
Clean an email list in five steps
Before you start: why a list needs cleaning
Bounces damage your sending reputation, and reputation damage lands on every message you send afterwards, including the ones to people who wanted to hear from you. A list that was fine two years ago has lost addresses to job changes, closed domains and typos nobody caught at signup. Sending and watching the bounce report works, but it teaches you which addresses were bad only after the damage is done.
A dedicated verification service solves this, usually with a bulk upload flow, per-address credits and sometimes a minimum purchase. That suits a one-off file. It suits a workflow less well when the check has to run inside a signup form, inside a script that also enriches the list, or inside an agent that decides what to do next. This post covers the second case: the check is one request, with the same key you use for the rest of the catalog. Pricing and terms at other vendors change, so check their current pages if you are comparing.
Step 1: Normalise and deduplicate the export
Cleaning starts before the first request. Export the list to a CSV and do the cheap work locally, because every duplicate you remove is a point you do not spend.
- —Trim whitespace and lower-case the address. Mail domains are case-insensitive, so
Jane.Doe@ACME.fiandjane.doe@acme.fiare one row. - —Drop rows with no
@, with spaces inside the address, or with an empty domain. These are not worth a request. - —Deduplicate on the normalised address and keep the first row's metadata, or merge it if your CRM has several owners for one address.
- —Split off role addresses such as
info@orsales@if your campaign targets people. This is a business decision, not a check, so make it deliberately.
Keep the original file untouched and write the results to a new one. You will want to compare the two, and you will want to be able to rerun a failed batch without redoing the whole thing.
Step 2: Run one validation per address
Email validation takes one address per request. The request and the response are shown in the next section. What matters for the workflow is that the call is small and the same shape every time, which makes it simple to loop.
A modest number of parallel workers is plenty for a list of this size. Check the API documentation for current rate limits before you raise the concurrency, and back off with a pause and a retry when a request fails instead of firing again immediately.
Step 3: Read three fields, not one
The verdict alone is not enough. Three fields together decide what happens to a row:
- —
resultis the verdict. Anything other than a valid verdict leaves the send list. - —
isCatchAllsays whether the domain accepts mail to any address. On a catch-all domain a positive result proves less than it appears to, because the server would say yes to a made-up mailbox too. - —
confidencesays how strongly the verdict is supported. Treat a lower rating as a reason for caution even when the verdict is positive.
The catch-all flag is the field people underuse. A catch-all domain limits what any validator can tell you, and that is a property of the domain, not a weakness of the check. Knowing it up front is what stops you from treating a guess as a verified address.
Step 4: Sort the rows into buckets
Write the policy down once and apply it to every row. A reasonable starting default looks like this, to be adjusted against your own bounce history:
| What the check returns | A sensible default for the list |
|---|---|
result is not valid | Remove from the send list. Keep the row and the reason in a suppression file so the address is not re-imported next month. |
Valid, isCatchAll false, high confidence | Keep. This is the clean core of the list. |
Valid, isCatchAll true | Keep but mark as unconfirmed. Send these in a later, smaller wave and watch the bounce rate before widening. |
Valid, lower confidence | Treat like a catch-all: keep, mark, send late. |
| The request itself failed (timeout, error) | Retry a few times with a pause, then park the row for a later pass. Do not treat a failed call as a bad address. |
The buckets matter more than the exact labels. You end up with a clean core you can send to now, a cautious group you send to later in smaller volume, and a suppression list. The suppression list is an asset: keep it, and check new imports against it so the same dead address does not come back.
Step 5: Write the result back and keep the evidence
Save a results file with the original row, the verdict, the catch-all flag, the provider, the confidence and the date you checked. The date matters. A valid verdict describes the address now, so a list checked in November should be rechecked before a campaign next spring. For a list that feeds a CRM, write the bucket into a field the sending tool can filter on, so nobody has to remember which export was the clean one.
Request and response
What the response contains
These are the fields the source page documents. Use them as written; the response envelope is the same as for every other source in the catalog.
| Field | Type | What it tells you |
|---|---|---|
| string | The address that was checked | |
| result | string | The verdict for the address |
| message | string | An explanation of the verdict |
| isCatchAll | boolean | Whether the domain accepts mail to any address, which limits what a check can prove |
| provider | string | The mail provider behind the domain |
| confidence | string | How strongly the verdict is supported |
A real request and response
One address in, one verdict out. The request below is the apiCall from the Email validation source page, using the illustrative address jane.doe@acme.com.
curl -X POST https://api.neuralverge.ai/functions/v1/run-email-validation \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "jane.doe@acme.com" }'
The sample response from the source page looks like this. The address has been replaced with an illustrative one; the other values are as published.
{
"session_id": "11111111-1111-1111-1111-111111111111",
"kind": "email_validation",
"email": "jane.doe@acme.com",
"human": "# Email validation\n\n- **Email:** jane.doe@acme.com\n- **Result:** valid\n- **Message:** Mailbox exists and accepts mail.\n- **Catch-all:** No\n- **Provider:** google_workspace\n- **Confidence:** high",
"machine": {
"email": "jane.doe@acme.com",
"result": "valid",
"message": "Mailbox exists and accepts mail.",
"isCatchAll": false,
"provider": "google_workspace",
"confidence": "high"
},
"total_points": 1
}
The human string is a readable summary, handy when an agent reports back to a person. A script reads the machine object. The total_points value is the cost of that one check.
REST and MCP
The same check is reachable over REST and over MCP. NeuralVerge hosts the MCP server at https://mcp.neuralverge.ai/mcp. You add the URL to an MCP client such as Claude, ChatGPT, Cursor or VS Code, sign in through the browser and click Allow. There is no key to paste into a config file and nothing to host. Per-client steps are in the connect a client guide, and for Claude see the MCP server for Claude.
Over MCP the tool is run_email_validation, with underscores, while the REST path uses hyphens. An agent call mirrors the REST request:
{
"tool": "run_email_validation",
"arguments": { "email": "jane.doe@acme.com" }
}
The response has the same envelope and the same 1-point price; there is no separate MCP rate. Which one to pick depends on the list.
- —A script over REST suits a full export. It reads the CSV, runs the loop, retries failures and writes the results file. This is the right tool for 10,000 rows.
- —An agent over MCP suits small lists and judgement calls. Ask it to check the twenty addresses from a conference badge scan, report which are catch-all and draft a note on how to handle them. It chooses its next step based on what comes back.
Here is a compact REST loop for a full list. It reads the key from the environment, checks a thread pool of eight workers, retries a failed request, and writes everything it learned to a new file. Treat it as a starting point and tune the worker count to the documented limits.
import csv, os, time, requests
from concurrent.futures import ThreadPoolExecutor
URL = "https://api.neuralverge.ai/functions/v1/run-email-validation"
HEADERS = {"Authorization": f"Bearer {os.environ['NV_API_KEY']}"}
def check(email, tries=3):
for attempt in range(tries):
try:
r = requests.post(URL, headers=HEADERS, json={"email": email}, timeout=60)
r.raise_for_status()
m = r.json()["machine"]
return [email, m["result"], m["isCatchAll"], m["provider"], m["confidence"], ""]
except Exception as e:
time.sleep(2 * (attempt + 1))
return [email, "", "", "", "", "request failed"]
with open("emails_deduped.txt") as f:
emails = [line.strip().lower() for line in f if line.strip()]
with ThreadPoolExecutor(max_workers=8) as pool:
rows = list(pool.map(check, emails))
with open("emails_checked.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["email", "result", "isCatchAll", "provider", "confidence", "error"])
w.writerows(rows)
Rows with a non-empty error column go into a retry file, not the suppression list. A failed request tells you about the call, not the address.
Cost and limits
The source page states the price as 1 point per check, which it also gives as $1 per 1,000. Every plan on the pricing page works out to $1 per 1,000 points, so converting points to dollars is straightforward. The table below is derived from that, not published as a price list.
| List size | Points | Dollars (derived) |
|---|---|---|
| 1,000 addresses | 1,000 | $1 |
| 10,000 addresses | 10,000 | $10 |
| 50,000 addresses | 50,000 | $50 |
| 100,000 addresses | 100,000 | $100 |
For scale, the Base plan includes 50,000 points a month, so cleaning a 10,000-row list uses a fifth of its monthly allowance. Check the pricing page for the current plan sizes.
- —Billing: the source page says 1 point per check. It does not say that failed or unresolved requests are free, so do not budget on that assumption. Run a small sample first and read
total_pointson each response. - —Catch-all domains: no validator can confirm a specific mailbox on one. The flag tells you when you are in that situation, and your policy decides what to do about it.
- —Freshness: the check runs at request time, but a valid verdict describes the address now, not next quarter.
- —What it does not check: deliverability only. It does not tell you whether the person still works at the company.
- —Compliance: an address is personal data. Handle it under your own lawful basis and honour opt-outs. A clean result is not consent.
Pitfalls
Treating a catch-all as a confirmed address
On a catch-all domain a positive result is weak evidence. Send those addresses later and in smaller volume, and watch what happens before you widen the send.
Deleting instead of suppressing
If you delete a bad address from the CRM, the next import brings it back. Keep a suppression list and check new rows against it.
Cleaning once and forgetting
A clean list decays. Job changes and closed mailboxes accumulate, so schedule a recheck before large campaigns, and validate new addresses at the point they enter the list. At 1 point each, checking a signup inline is inexpensive.
Validating before deduplicating
Paying for the same address twice is the easiest waste to avoid. Deduplicate on the normalised address first.
Reading a failed request as a bad address
A timeout says nothing about the mailbox. Retry, and park what still fails for a later pass.
Skipping the sample
Run a few hundred rows first and look at the spread of results. If a large share comes back catch-all, your list leans on a few big domains and the policy needs to account for that before the full run.
How to decide
- —Is the list older than a year, imported or merged? Then clean it before the next send. Those are the lists that lose the most addresses.
- —Did the addresses come from Email finder or a guess pattern? Validate every one before it enters a send list. Email finder returns a best match, and validation is the cheaper second step.
- —Do you need it inside a form or an agent loop? One request per address, 1 point each, with the same key: that fits better than a separate bulk upload tool.
- —Will you act on the catch-all flag? If not, you are using a fraction of what the check returns. Decide the policy before the run.
- —Is the list a one-off file or a recurring feed? A file suits a script run once. A feed suits a check at the point of entry plus a scheduled recheck.
- —Do you have a lawful basis to email these people? Validation cannot answer that. Settle it with counsel before the campaign.
Frequently asked questions
How much does it cost to clean 10,000 email addresses?
10,000 points, which is $10 at the rate every plan works out to ($1 per 1,000 points). Email validation costs 1 point per address according to its source page. The dollar figure is derived from the plan prices, so check the pricing page for the current numbers before you plan a large run.
What is a catch-all domain and why does it matter for list cleaning?
A catch-all domain is configured to accept mail sent to any address at it. Because of that, no check can prove that a specific mailbox exists there. The isCatchAll field tells you when you are in that situation, so you can treat a positive result on such a domain as weaker evidence and send to it more cautiously.
Will cleaning my list stop all bounces?
No. It removes addresses that are clearly bad and flags the cases where certainty is not available. Mailboxes still fill up and people still leave their jobs after the check, so a clean result describes the address at the time of the check, not next quarter.
Can an AI agent clean an email list over MCP?
Yes. Add the hosted MCP server at https://mcp.neuralverge.ai/mcp to an MCP client, sign in and click Allow, and run_email_validation appears as a tool. It takes one address per call and costs the same 1 point as the REST request, so MCP suits small lists and spot checks while a script suits a full export.
Do I need a separate bulk upload tool for a large list?
Not with this approach. Email validation takes one address per request, so a large list is a loop in a script or a job in your own tooling. There is no upload flow to learn, and the same API key works for the other sources.
Should I validate an address I just found with Email finder?
Yes. Email finder returns an address that is a best match, and validation is a separate, much cheaper call that tells you whether it is deliverable. Run it before the address enters a send list.
Is it legal to run a purchased or old list through validation?
An email address is personal data, and checking it does not give you a lawful basis to send to it. Validation tells you about deliverability, not consent. Your own counsel decides whether you may email a given list, and opt-outs must be honoured whatever the check returns.
Related articles
A b2b data api for finding leads, employees, emails and phones over REST or MCP: what each lookup needs as input, real JSON and what each step costs in points.
What is data enrichment? The fields it adds, how a lookup works, what match rate means and how to measure it on your own list, over REST or MCP.
A B2B lead search API turns filters like title, seniority, country and tech stack into a list of people at 2 points per record. See a real MCP response.
About NeuralVerge
Give your agents structured, cited, real-world data from 300+ sources and the open web — through one API or MCP server.
Data Sources on the NeuralVerge blog.