Skip to main content
New

1,300+ funded startups directory

Browse
LeadMagic logo
LeadMagic
Back to blog
Email Verification13 min read

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.

JO

Jesse Ouellette

February 22, 2026

Email Verification API: The Developer's Integration Guide

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:

  1. Syntax validation — Is the address properly formatted? (catches ~3% of bad inputs)
  2. MX record lookup — Does the domain have mail exchange records? Can it receive email?
  3. SMTP handshake — Connect to the mail server, initiate a conversation, and ask "does this mailbox exist?" — without sending an actual email
  4. Catch-all resolution — If the server accepts everything (catch-all), run proprietary multi-signal analysis to determine whether this specific address is genuinely valid
  5. 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

PlanRequests/SecondRequests/MinuteDaily Limit
Starter51005,000
Growth2050050,000
EnterpriseCustomCustomUnlimited

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.

FieldTypeDescription
emailstringThe email address that was verified
statusstringOverall result: valid, invalid, risky, unknown
is_validbooleanWhether the mailbox exists and can receive email
is_catchallbooleanWhether 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_disposablebooleanWhether the address is from a disposable/temporary email service
is_rolebooleanWhether the address is a role account (info@, support@, admin@, etc.)
mx_foundbooleanWhether the domain has valid MX records configured
confidencenumberConfidence 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:

  1. Trigger: New contact created in HubSpot/Salesforce
  2. HTTP Request: Call LeadMagic verification API
  3. IF node: Check is_valid and confidence
  4. True branch: Tag contact as "verified," add to sequences
  5. 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:

  1. Column 1: Input data (name + company)
  2. Column 2: Email finder (LeadMagic or waterfall)
  3. Column 3: LeadMagic email verification on the found address
  4. 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.

Get your API key in 30 seconds

100 free credits. No credit card. API, CLI, and MCP — all from one key.