How to Verify Emails in a pandas DataFrame With the SimpleVerifier API
To verify emails in a pandas DataFrame, normalise and dedupe the email column, call a verification API once per unique address, build a small DataFrame of the results, and merge it back on the cleaned address. pandas can check that an address is shaped correctly, but it cannot know whether a mailbox exists; that needs a live mail-server check. The SimpleVerifier API returns valid, risky or invalid with a reason for each address. Pro is $39 a month with 10,000 API calls included, and you can try 100 free checks first.
If the file is large and you just want it cleaned, you do not need a script: upload the CSV on the verify page instead. The script below is for pipelines, notebooks, and lists that are already in a DataFrame.
Why verify before you send
A DataFrame full of scraped, exported or purchased addresses will contain dead mailboxes, typos and disposable domains. Mailing them produces bounces, and bounces count against your sending domain and IP reputation. Verification is the cheapest step you can add before a send. See why cold emails bounce for the causes.
What the API returns
GET https://simpleverifier.com/api/v1/verify?email=jane@company.com with an Authorization: Bearer header returns JSON. The fields you will use most:
status:valid,riskyorinvalid.reason: a short label such as Mailbox exists, Accepts all addresses or Mailbox not found.send_advice:safe,care,retryordo_not_send.action: the machine form of the advice, one ofsend,send_with_care,retry_laterordo_not_send.
Create a key on the API keys page; keys start with sv_live_ and are shown once. Keep it in an environment variable, never in the notebook. The full field list is in the API docs.
Limits to design around: each key can make 120 calls a minute, and a 429 response includes a Retry-After header. Pro includes 10,000 API calls a month. If your DataFrame has more unique addresses than that, use the CSV upload for the bulk and keep the API for the new rows.
Step 1: Clean and dedupe the column
import os
import time
import pandas as pd
import requests
df = pd.read_csv("leads.csv")
df["email_clean"] = df["email"].astype(str).str.strip().str.lower()
df = df[df["email_clean"].str.contains("@", na=False)]
unique = df["email_clean"].drop_duplicates().tolist()
print(len(df), "rows,", len(unique), "unique addresses")
Deduping matters twice over: it saves checks, and it stops you mailing one person twice. The duplicate email remover does the same job outside Python. Do not use the contains("@") test as validation; it only drops cells that cannot possibly be addresses.
Step 2: Check each unique address
KEY = os.environ["SIMPLEVERIFIER_KEY"]
URL = "https://simpleverifier.com/api/v1/verify"
def check(address, retries=3):
for attempt in range(retries):
r = requests.get(
URL,
params={"email": address},
headers={"Authorization": f"Bearer {KEY}"},
timeout=45,
)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
if r.status_code == 503:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json()
return {"status": "risky", "reason": "no answer", "send_advice": "retry"}
Two details matter. A 429 means slow down, and the Retry-After header says for how long. A timeout or 503 is not a verdict, so the fallback marks the row risky with a retry advice rather than calling it valid or invalid. The API error reference lists every status code.
If you would rather not write the retry loop, the official client does it. pip install simpleverifier, then sv = SimpleVerifier() reads the key from SIMPLEVERIFIER_KEY, and sv.verify_emails(emails, concurrency=5) verifies a list with bounded concurrency and reports each address's result or error without stopping the run. See the SDK docs for the details.
Step 3: Save as you go, then merge
results = []
for i, address in enumerate(unique, 1):
out = check(address)
results.append({
"email_clean": address,
"sv_status": out.get("status"),
"sv_reason": out.get("reason"),
"sv_advice": out.get("send_advice"),
})
if i % 200 == 0:
pd.DataFrame(results).to_csv("results_partial.csv", index=False)
res = pd.DataFrame(results)
final = df.merge(res, on="email_clean", how="left")
final.to_csv("leads_verified.csv", index=False)
The checkpoint file means a crash at address 4,000 costs you nothing. For a long run, load results_partial.csv at the start and skip addresses already in it. Our general resumable CSV script shows that pattern in full. The loop above is sequential, which at 120 calls a minute is the ceiling anyway; use a few threads only if you stay under that rate.
Step 4: Split by result
valid = final[final["sv_status"] == "valid"]
risky = final[final["sv_status"] == "risky"]
invalid = final[final["sv_status"] == "invalid"]
valid.to_csv("send_now.csv", index=False)
risky.to_csv("send_small_batches.csv", index=False)
What each status means:
| Status | Meaning | What to do |
|---|---|---|
| valid | The mail server confirmed the mailbox exists. | Safe to send. |
| risky | Delivery is not confirmed: a catch-all domain, a role address such as info@, or a provider that will not say. | Keep out of large sends. Test in a small batch. |
| invalid | Bad format, no mail server, disposable address or mailbox not found. | Do not send. |
Risky rows deserve their own file. A catch-all domain accepts every address, so a mailbox cannot be confirmed. Send those in small batches and watch the bounces; the catch-all send limit calculator helps you pick a size. Rows whose advice is retry did not get an answer from the mail server; run them again later instead of treating them as bad.
Checking a single address in a notebook
For a quick look at one address, you do not need pandas: paste it into the free email checker or use sv.verify_email("jane@company.com") from the Python client. Single-address checks from the web tool need no account.
When the CSV upload is better
The API path costs you code and the monthly API allowance. The CSV path does not. If the DataFrame is a one-off clean, call final[["email_clean"]].to_csv("to_check.csv"), upload it on the verify page, and merge the downloaded results back with df.merge. The first 100,000 checks a month on Pro run at full speed, then slower, never cut off. The CSV email verifier page explains the format. See also API or CSV upload: which to use.
Common mistakes in a notebook
- Calling the API once per row, duplicates included. Dedupe first. A list with repeated addresses pays for each repeat otherwise.
- Putting the key in the notebook. A notebook gets shared, committed and exported. Read the key from an environment variable and keep it out of saved outputs.
- Treating a failed call as a result. Rows with no answer should stay unverified, not default to valid or invalid.
- Overwriting the original file. Write to a new file so you can always go back to what you started with.
- Forgetting to re-run. A saved results file is a snapshot. Before the next campaign, check the rows that are older than your comfort level, or new since the last run.
What it costs
SimpleVerifier Pro is $39 a month. The first 100,000 checks a month run at full speed, then they run slower, and you are never cut off. You can try 100 free checks first by signing in with Google, no card. See pricing for the other plans. API access comes with Pro, which includes 10,000 calls a month at 120 calls a minute per key.
Bottom line
pandas is good at the cleaning and the merge; it cannot confirm a mailbox. Dedupe, check each unique address once with retries, save as you go, and split the file by status before anything is sent. Run the 100 free checks on a sample of your DataFrame first and look at what share comes back risky or invalid.
Common questions
How do I verify an email column in pandas?
Clean and lower-case the column, drop duplicates, send each unique address to a verification API, collect the status and reason into a second DataFrame, and merge it back onto the original on the cleaned address. Save the result to CSV.
Does pandas have a built-in way to validate email addresses?
No. You can use str.contains with a pattern to check the shape of an address, but that cannot tell whether the mailbox exists. For that you need a DNS and SMTP check, which an email verification service performs.
What is the rate limit for the SimpleVerifier API?
Each key can make 120 calls a minute. Past that the API answers 429 with a Retry-After header. The official Python client paces itself and retries rate-limit responses for you.
Should I use the API or upload the CSV?
Use the API when verification is one step in a pipeline or the list is small. For big one-off files, uploading the CSV on the verify page is simpler, and the first 100,000 checks a month on Pro run at full speed.
How much does it cost?
100 free checks with a Google sign-in. Pro is $39 a month, including 10,000 API calls a month, with the first 100,000 CSV checks a month at full speed, then slower, never cut off.
Check your list before you send
Real mailbox checks over SMTP, and nothing is sent to the address. Sign in with Google and your first 100 email checks are free, no card. Plans for bigger lists are on the pricing page.