Prerequisites
- Python 3.8 or higher
- The
requestslibrary (installed below) - An ip-api.io API key — get one free
Install requests
Install the requests library if you don't have it already. Pin the version
in requirements.txt for reproducible installs.
pip install requests # requirements.txt
requests>=2.28 Quickstart: verify a single email
The batch endpoint accepts an array of up to 100 addresses — pass a single-element list for individual checks. The API performs a live SMTP handshake and returns the result in one response. Store your API key in an environment variable — never hardcode it.
import os
import requests
API_KEY = os.environ["IP_API_IO_KEY"]
resp = requests.post(
"https://ip-api.io/api/v1/email/advanced/batch",
headers={"X-Api-Key": API_KEY},
json={"emails": ["user@example.com"]},
timeout=30,
)
resp.raise_for_status()
results = resp.json()
data = results["user@example.com"]
print(data["reachable"]) # "yes", "no", or "unknown"
print(data["disposable"]) # False
print(data["role_account"]) # False
print(data["smtp"]["catch_all"]) # False
print(data["has_mx_records"]) # True
print(data["suggestion"]) # None, or e.g. "user@gmail.com" for typos X-Api-Key header with your API key to verify larger lists —
subscribe at ip-api.io/pricing to get started.
Batch verify a list
For lists larger than 100 addresses, split into chunks of 100 and collect all results before categorizing. The API processes each batch concurrently — a full 100-address batch typically completes in 15–60 seconds depending on SMTP server response times.
import os
import csv
import requests
from itertools import islice
API_KEY = os.environ["IP_API_IO_KEY"]
def _chunks(iterable, size):
it = iter(iterable)
while chunk := list(islice(it, size)):
yield chunk
def verify_emails(emails: list[str]) -> dict:
"""Verify any number of emails, 100 per API request."""
all_results = {}
for chunk in _chunks(emails, 100):
resp = requests.post(
"https://ip-api.io/api/v1/email/advanced/batch",
headers={"X-Api-Key": API_KEY},
json={"emails": chunk},
timeout=90,
)
resp.raise_for_status()
all_results.update(resp.json())
return all_results
def categorize(data: dict) -> str:
if data["reachable"] == "yes" and not data["disposable"]:
return "clean"
if data["reachable"] == "no" or data["disposable"]:
return "invalid"
return "risky"
emails = ["alice@example.com", "test@mailinator.com", "bob@company.org"]
results = verify_emails(emails)
clean = [e for e, d in results.items() if categorize(d) == "clean"]
risky = [e for e, d in results.items() if categorize(d) == "risky"]
invalid = [e for e, d in results.items() if categorize(d) == "invalid"]
print(f"Clean: {len(clean)}, Risky: {len(risky)}, Invalid: {len(invalid)}")
# Write the clean list to a CSV file
with open("clean_list.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["email"])
writer.writerows([[e] for e in clean]) Upload a CSV file
The CSV endpoint accepts a multipart/form-data POST with a CSV or plain-text
file up to 1MB. The API reads the first column as the email address and
auto-detects header rows. Use requests' files= parameter —
no need to set Content-Type manually.
import os
import requests
API_KEY = os.environ["IP_API_IO_KEY"]
def verify_csv(path: str) -> dict:
"""Upload a CSV file for batch verification.
First column used as email; header row auto-detected; max 1MB per request.
For files larger than 1MB, split and send multiple requests.
"""
with open(path, "rb") as f:
resp = requests.post(
"https://ip-api.io/api/v1/email/advanced/batch/csv",
headers={"X-Api-Key": API_KEY},
files={"file": f},
timeout=90,
)
resp.raise_for_status()
return resp.json()
results = verify_csv("email_list.csv")
print(f"Verified {len(results)} addresses")
# Extract clean addresses
clean = [
email for email, data in results.items()
if data["reachable"] == "yes" and not data["disposable"]
]
print(f"{len(clean)} deliverable addresses") @ sign it is treated as a header and skipped.
Comma, semicolon, and tab delimiters are all accepted.
Production-ready helper
A complete EmailListCleaner class with error handling for timeouts,
rate limits, and HTTP errors, plus a method to clean a CSV file in one call.
import os
import csv
import requests
from itertools import islice
API_KEY = os.environ["IP_API_IO_KEY"]
_BATCH_URL = "https://ip-api.io/api/v1/email/advanced/batch"
_CSV_URL = "https://ip-api.io/api/v1/email/advanced/batch/csv"
def _chunks(iterable, size):
it = iter(iterable)
while chunk := list(islice(it, size)):
yield chunk
class EmailListCleaner:
"""Batch email verifier using the ip-api.io SMTP verification API."""
def __init__(self, api_key: str = API_KEY, timeout: int = 90):
self.session = requests.Session()
self.session.headers["X-Api-Key"] = api_key
self.timeout = timeout
def verify_batch(self, emails: list[str]) -> dict:
"""Verify up to 100 emails in one API request.
Returns a dict keyed by email address.
Raises RuntimeError on HTTP errors or rate limiting.
"""
try:
resp = self.session.post(
_BATCH_URL,
json={"emails": emails},
timeout=self.timeout,
)
if resp.status_code == 429:
raise RuntimeError(
"Rate limit reached — upgrade your plan or add an API key"
)
resp.raise_for_status()
return resp.json()
except requests.Timeout:
raise RuntimeError(
"Request timed out (SMTP verification can take up to 60s for 100 addresses)"
)
except requests.HTTPError as exc:
raise RuntimeError(f"API error HTTP {exc.response.status_code}") from exc
def clean_list(
self, emails: list[str]
) -> tuple[list[str], list[str], list[str]]:
"""Verify a list of any size. Returns (clean, risky, invalid)."""
clean, risky, invalid = [], [], []
for chunk in _chunks(emails, 100):
results = self.verify_batch(chunk)
for email, data in results.items():
if data["reachable"] == "yes" and not data["disposable"]:
clean.append(email)
elif data["reachable"] == "no" or data["disposable"]:
invalid.append(email)
else:
risky.append(email)
return clean, risky, invalid
def clean_csv(self, input_path: str, output_path: str) -> int:
"""Read input_path CSV, verify all addresses, write clean ones to output_path.
Returns the number of deliverable addresses written.
"""
with open(input_path, newline="") as f:
rows = list(csv.reader(f))
start = 1 if rows and "@" not in rows[0][0] else 0
emails = [row[0].strip() for row in rows[start:] if row]
clean, _, _ = self.clean_list(emails)
with open(output_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["email"])
writer.writerows([[e] for e in clean])
return len(clean)
# Usage
cleaner = EmailListCleaner()
n = cleaner.clean_csv("raw_list.csv", "clean_list.csv")
print(f"{n} deliverable addresses written to clean_list.csv")
Prefer async Python? Use httpx as a drop-in replacement:
import httpx
import os
API_KEY = os.environ["IP_API_IO_KEY"]
async def verify_emails_async(emails: list[str]) -> dict:
async with httpx.AsyncClient(
headers={"X-Api-Key": API_KEY},
timeout=90,
) as client:
resp = await client.post(
"https://ip-api.io/api/v1/email/advanced/batch",
json={"emails": emails},
)
resp.raise_for_status()
return resp.json()