Email Verification API: The Developer's Integration Guide
Integrate email verification with curl, Python, and Node.js. Covers responses, catch-all handling, batching, and production error strategies.
Jesse Ouellette
February 22, 2026
I get asked the same question at least once a week: "Can't I just regex-check emails and call it done?" You can. And you'll end up with a database full of addresses that look valid but bounce when you actually try to send something. Syntax validation catches maybe 3% of bad emails. The other 97% of problems — inactive mailboxes, catch-all traps, disposable addresses, role accounts — require actually talking to the mail server.
That's what a verification API does. It connects to the recipient's mail infrastructure, runs a multi-step handshake, and gives you a definitive answer: this address will receive your email, or it won't. If you're building enrichment into a product, powering a signup form, or cleaning CRM data programmatically, this guide covers everything you need to ship a production integration.
Why Use a Verification API
There are three ways to verify email addresses: manually (checking one at a time through a web tool), bulk CSV upload, and API. The API approach wins when any of these are true:
Real-Time Validation
You need to verify addresses as they come in — signup forms, lead capture, CRM imports. A CSV workflow adds hours of latency. An API call takes under 200ms and returns a result before the user even sees a loading spinner.
Programmatic Integration
Your application needs to make decisions based on verification results. Is this a valid address? Is it catch-all? Is it disposable? These aren't questions you want a human answering manually. Structured API responses let your code branch on the result automatically.
Volume and Consistency
When you're verifying thousands of addresses daily, you need consistent throughput, predictable response formats, and automatic error handling. APIs give you all three. A developer writes the integration once, and it runs forever without human intervention.
Cost Control
API-based verification with pay-per-result pricing means you pay for exactly what you use. No wasted credits, no seat fees, no minimum commitments. At LeadMagic's pricing, verification starts at $59.99/mo and scales linearly with volume.
API Architecture Overview
Before writing code, it helps to understand what's happening under the hood when you make a verification request.
The Verification Pipeline
When you send an email address to our API, it runs through five sequential checks:
- Syntax validation — Is the address properly formatted? (catches ~3% of bad inputs)
- MX record lookup — Does the domain have mail exchange records? Can it receive email?
- SMTP handshake — Connect to the mail server, initiate a conversation, and ask "does this mailbox exist?" — without sending an actual email
- Catch-all resolution — If the server accepts everything (catch-all), run proprietary multi-signal analysis to determine whether this specific address is genuinely valid
- Disposable/role detection — Flag temporary email services and generic addresses (info@, support@, admin@)
The entire pipeline completes in under 200ms for most addresses. Catch-all resolution occasionally takes slightly longer because it involves additional verification steps.
Authentication
All requests authenticate via the X-API-Key header. Generate your key from the LeadMagic dashboard. Store it as an environment variable — never hardcode API keys.
export LEADMAGIC_API_KEY="your_api_key_here"
Base URL
https://api.leadmagic.io/v1
Rate Limits
| Plan | Requests/Second | Requests/Minute | Daily Limit |
|---|---|---|---|
| Starter | 5 | 100 | 5,000 |
| Growth | 20 | 500 | 50,000 |
| Enterprise | Custom | Custom | Unlimited |
Rate-limited requests return HTTP 429 with a Retry-After header.
Getting Started: Your First Verification
Let's verify a single email address in three languages. Each example includes proper error handling — copy-paste these into your project and swap in your API key.
curl
The fastest way to test. Run this in your terminal:
curl -X POST https://api.leadmagic.io/v1/email/verify \
-H "Content-Type: application/json" \
-H "X-API-Key: $LEADMAGIC_API_KEY" \
-d '{
"email": "jane.smith@acme.com"
}'
A successful response:
{
"success": true,
"data": {
"email": "jane.smith@acme.com",
"status": "valid",
"is_valid": true,
"is_catchall": false,
"is_disposable": false,
"is_role": false,
"mx_found": true,
"confidence": 99
}
}
Python (requests)
A production-ready implementation with retry logic:
import os
import requests
from time import sleep
API_KEY = os.environ["LEADMAGIC_API_KEY"]
BASE_URL = "https://api.leadmagic.io/v1"
def verify_email(email: str, retries: int = 3) -> dict | None:
"""Verify an email address with automatic retry on rate limits."""
for attempt in range(retries):
response = requests.post(
f"{BASE_URL}/email/verify",
headers={
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
json={"email": email},
timeout=30,
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
sleep(retry_after)
continue
if response.status_code == 401:
raise Exception("Invalid API key — check LEADMAGIC_API_KEY")
response.raise_for_status()
result = response.json()
if result.get("success"):
return result["data"]
return None
raise Exception(f"Max retries exceeded for {email}")
# Usage
result = verify_email("jane.smith@acme.com")
if result:
print(f"Status: {result['status']}")
print(f"Valid: {result['is_valid']}")
print(f"Catch-all: {result['is_catchall']}")
print(f"Disposable: {result['is_disposable']}")
print(f"Confidence: {result['confidence']}%")
else:
print("Verification failed — address likely invalid")
Node.js (fetch)
Modern JavaScript with async/await:
const API_KEY = process.env.LEADMAGIC_API_KEY;
const BASE_URL = "https://api.leadmagic.io/v1";
async function verifyEmail(email, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
const response = await fetch(`${BASE_URL}/email/verify`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify({ email }),
});
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || 2 ** attempt);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
if (response.status === 401) {
throw new Error("Invalid API key — check LEADMAGIC_API_KEY");
}
if (!response.ok) {
const error = await response.json();
throw new Error(`API error ${response.status}: ${error.error}`);
}
const result = await response.json();
return result.success ? result.data : null;
}
throw new Error(`Max retries exceeded for ${email}`);
}
// Usage
const result = await verifyEmail("jane.smith@acme.com");
if (result) {
console.log(`Status: ${result.status}`);
console.log(`Valid: ${result.is_valid}`);
console.log(`Catch-all: ${result.is_catchall}`);
console.log(`Disposable: ${result.is_disposable}`);
console.log(`Confidence: ${result.confidence}%`);
} else {
console.log("Verification failed — address likely invalid");
}
Response Field Reference
Every verification response includes the same fields. Here's what each one means and how to use it in your application logic.
| Field | Type | Description |
|---|---|---|
email | string | The email address that was verified |
status | string | Overall result: valid, invalid, risky, unknown |
is_valid | boolean | Whether the mailbox exists and can receive email |
is_catchall | boolean | Whether the domain is a catch-all (accepts all addresses). LeadMagic resolves catch-alls to valid/invalid, so this flag indicates the domain type, not ambiguity |
is_disposable | boolean | Whether the address is from a disposable/temporary email service |
is_role | boolean | Whether the address is a role account (info@, support@, admin@, etc.) |
mx_found | boolean | Whether the domain has valid MX records configured |
confidence | number | Confidence score from 0-100. For valid addresses, this is typically 95-99. For catch-all resolutions, it may be lower |
Decision Matrix
Here's how I'd recommend using these fields in your application:
def should_send(result: dict) -> tuple[bool, str]:
"""Decide whether to send to this address based on verification result."""
if not result["is_valid"]:
return False, "invalid"
if result["is_disposable"]:
return False, "disposable"
if result["is_role"]:
return False, "role_account"
if result["is_catchall"] and result["confidence"] < 70:
return False, "low_confidence_catchall"
return True, "approved"
Most teams use a confidence threshold of 70-80 for catch-all addresses. Below 70, the risk of bouncing outweighs the potential value. Above 80, you're almost certainly reaching a real inbox. Adjust based on your tolerance — if you're sending high-volume cold outreach, be more conservative. If you're sending transactional emails to existing customers, you can be more aggressive.
Handling Catch-All Domains
Catch-all domains are the biggest source of confusion in email verification. About 30-40% of B2B domains are configured as catch-all — the server accepts email for any address, even ones that don't have real mailboxes.
Most verification APIs punt on this. They return "catch-all" as a status and leave you to guess. That's not useful when you're trying to decide whether to send to 40,000 addresses.
LeadMagic's catch-all validation resolves these to actionable results. Instead of "this domain is catch-all, good luck," you get "this specific address at this catch-all domain is valid with 87% confidence" or "this address is invalid." The difference in your campaign performance is measurable — teams that properly handle catch-alls see 15-25% more reachable addresses compared to teams that either skip them entirely or send blindly.
Here's how to handle catch-all results in your code:
def handle_catchall(result: dict) -> str:
"""Route catch-all results based on confidence."""
if not result["is_catchall"]:
return "standard_verification"
confidence = result["confidence"]
if confidence >= 85:
return "send_normally"
elif confidence >= 65:
return "send_with_monitoring"
else:
return "skip_or_manual_review"
Batch Verification via API
Single-address verification is fine for signup forms and real-time enrichment. But when you need to clean a list of 50,000 emails before importing them into your sequencer, you need batch processing.
Async Batch Implementation (Python)
import asyncio
import csv
import aiohttp
import os
API_KEY = os.environ["LEADMAGIC_API_KEY"]
BASE_URL = "https://api.leadmagic.io/v1"
MAX_CONCURRENT = 15
RATE_LIMIT_DELAY = 0.05
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
async def verify_single(session: aiohttp.ClientSession, email: str) -> dict:
"""Verify one email with retry logic."""
async with semaphore:
for attempt in range(3):
try:
async with session.post(
f"{BASE_URL}/email/verify",
json={"email": email},
headers={
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
) as resp:
if resp.status == 429:
wait = int(resp.headers.get("Retry-After", 2 ** (attempt + 1)))
await asyncio.sleep(wait)
continue
data = await resp.json()
return {
"email": email,
"status": data["data"]["status"] if data.get("success") else "error",
"is_valid": data["data"].get("is_valid") if data.get("success") else False,
"is_catchall": data["data"].get("is_catchall") if data.get("success") else None,
"confidence": data["data"].get("confidence") if data.get("success") else None,
}
except aiohttp.ClientError:
if attempt == 2:
return {"email": email, "status": "error", "is_valid": False,
"is_catchall": None, "confidence": None}
await asyncio.sleep(2 ** attempt)
await asyncio.sleep(RATE_LIMIT_DELAY)
async def verify_batch(input_csv: str, output_csv: str):
"""Verify all emails in a CSV and write results."""
with open(input_csv) as f:
emails = [row["email"] for row in csv.DictReader(f)]
print(f"Verifying {len(emails)} emails...")
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*[verify_single(session, e) for e in emails])
valid = sum(1 for r in results if r["is_valid"])
print(f"Results: {valid} valid / {len(results)} total ({valid/len(results)*100:.1f}%)")
with open(output_csv, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["email", "status", "is_valid", "is_catchall", "confidence"])
writer.writeheader()
writer.writerows(results)
# asyncio.run(verify_batch("emails.csv", "verified.csv"))
This processes around 50-100 emails per second with 15 concurrent connections. A 10,000-email list finishes in about 2-3 minutes.
If you'd rather skip code entirely, our CSV enrichment tool handles the same workflow with drag-and-drop upload. Upload your file, map the email column, and download verified results.
Integration Patterns
After working with hundreds of teams integrating our API, three patterns come up consistently.
Signup Form Validation
The highest-impact integration. Verify the email on form submission before it ever touches your database. Sub-200ms response times mean the user won't notice any latency. Block disposable addresses, flag role accounts, and only accept verified valid addresses.
async function handleSignup(email, name) {
const verification = await verifyEmail(email);
if (!verification?.is_valid) {
return { error: "Please enter a valid email address." };
}
if (verification.is_disposable) {
return { error: "Please use a work or personal email — temporary emails aren't accepted." };
}
// Proceed with signup
return createUser(email, name);
}
CRM Sync Pipeline
Run verification on every new contact entering your CRM. Whether leads come from form submissions, list imports, or third-party integrations, verify before they hit your marketing automation. This keeps your sender reputation clean and your engagement metrics accurate.
A typical flow with n8n or Make:
- Trigger: New contact created in HubSpot/Salesforce
- HTTP Request: Call LeadMagic verification API
- IF node: Check
is_validandconfidence - True branch: Tag contact as "verified," add to sequences
- False branch: Tag as "needs review," exclude from automated outreach
Enrichment Waterfall with Clay
In Clay workflows, verification typically runs as the final step after email finding. You find the email with one provider, then verify it with LeadMagic before loading it into your outreach tool. This two-step pattern catches the ~5-10% of found emails that turn out to be invalid — preventing bounces before they happen.
The Clay setup:
- Column 1: Input data (name + company)
- Column 2: Email finder (LeadMagic or waterfall)
- Column 3: LeadMagic email verification on the found address
- Column 4: Route to Instantly/Smartlead only if verification passes
Performance Characteristics
Numbers from production usage across our API:
- Median response time: 142ms
- 99th percentile response time: 380ms
- Catch-all resolution time: 180-400ms (additional verification steps)
- Throughput (Growth plan): ~20 requests/second sustained
- Uptime SLA: 99.9%
- Accuracy: 99.5% across our benchmark dataset of 500,000 verifications
These numbers hold at sustained throughput. We don't throttle during peak hours or degrade accuracy at higher volumes. The same verification pipeline runs whether you're sending 1 request or 1,000 per minute.
Error Handling and Retry Strategies
Production integrations need to handle four categories of API responses:
1. Success (HTTP 200, success: true)
The email was verified. Use the result fields to make routing decisions.
2. Not Verifiable (HTTP 200, success: false)
The API processed the request but couldn't reach a definitive result — usually because the mail server is temporarily unreachable or timing out. Don't treat this as "invalid." Retry after 30-60 minutes, as the target server may have been experiencing a transient issue.
3. Rate Limited (HTTP 429)
You've exceeded your plan's request limits. Read the Retry-After header and implement exponential backoff with jitter:
import random
import time
def backoff_with_jitter(attempt: int, base: float = 1.0, max_delay: float = 60.0) -> float:
"""Calculate backoff delay with jitter to prevent thundering herd."""
delay = min(base * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.5)
return delay + jitter
4. Server Error (HTTP 5xx)
Rare, but it happens. These are always retryable. Use the same backoff strategy as rate limits, with a maximum of 3-5 retries.
What's Never Retryable
- HTTP 401 — Invalid API key. Fix your credentials.
- HTTP 400 — Malformed request. Check your JSON body.
- HTTP 402 — Insufficient credits. Top up your account.
Webhook Alternative
For high-volume batch workflows where you don't want to maintain long-running connections, webhooks let you fire off verification requests and receive results asynchronously. Submit a batch of emails with a callback URL, and we POST each result to your endpoint as it completes.
This pattern works well for CRM enrichment pipelines and event-driven architectures where verification is one step in a larger processing chain. See our enrichment API documentation for webhook configuration details.
Email verification is one of those things that seems simple until you've processed your first 100,000 addresses. The edge cases — catch-all domains, greylisting servers, disposable services, temporarily unreachable mail servers — are where the difference between a 90% accurate tool and a 99.5% accurate tool becomes obvious in your bounce rates.
Start with the single-verification example above. Get it working against a handful of known addresses (test with your own email, a colleague's, and a known-bad address). Then scale up to batch processing when you're ready.
If you want to skip the integration work and verify emails through a UI, our email verification tool handles single lookups instantly, and the CSV upload processes bulk lists without code. For everything else, the API is waiting for you.
Related Posts
I ran 10,000 real B2B emails through 10 verification tools and measured accuracy, speed, catch-all handling, and cost. Here's what actually works.
Practical guide to cleaning email lists at scale: CSV workflow, 73K-email case study, processing speeds, and provider cost comparisons.
40% of enterprise domains are catch-all. Most verifiers label them 'risky.' Here's how we resolve them for real outbound decisions.