Email Verification API: A Developer's Guide to Real-Time Validation

A user signs up for your app. They type their email, hit submit, and your system fires off a welcome email. Except the address was [email protected] -- a typo that passed your regex check without issue. The welcome email bounces. The user never activates their account. You've lost them before they even started.

This is the gap that client-side validation cannot close. Regex patterns and HTML5 type="email" inputs catch malformed strings, but they tell you nothing about whether a mailbox actually exists or whether the address is a throwaway from a disposable email service.

An email verification API fills that gap. It checks the email in real time -- syntax, domain, MX records, SMTP handshake, disposable provider databases -- and returns a verdict in under two seconds.

This guide covers everything you need to integrate one: authentication, request and response formats, code examples in three languages, error handling, rate limiting, webhooks, and common integration patterns.

Why Real-Time Verification at Signup Matters

The cost of a bad email address compounds over time. If you catch it at the point of entry, you lose nothing -- the user corrects it and moves on. If you catch it later, you've already allocated resources: a database row, a welcome email that bounced, a drip campaign sending into the void, and a user who never received their account confirmation.

For a deeper look at the full verification process and why it matters, see What Is Email Verification and Why Does It Matter.

Real-time verification at signup gives you three things:

Immediate feedback. Users see "This email appears invalid -- did you mean [email protected]?" before they submit. Conversion rates improve because users can fix mistakes in the moment.

Clean data from day one. Every address in your database has been verified at the time of entry. You never accumulate a backlog of dead addresses that need bulk cleaning later.

Protection against abuse. Disposable emails, role-based addresses, and known spam traps are flagged before they enter your system. This is especially valuable for free-tier signups, trial accounts, and lead generation forms where fake addresses are common.

Authentication

EmailKit's API uses Bearer token authentication with API keys prefixed by ek_. You generate API keys from your EmailKit dashboard under the API Keys section.

Every request must include the key in the Authorization header:

Authorization: Bearer ek_your_api_key

API keys are shown in full exactly once -- at creation time. EmailKit stores only a hash and the key prefix, so if you lose the full key, you'll need to generate a new one.

Test mode keys are also available. They return mock responses without consuming credits, which is useful during development and CI/CD testing.

Single Email Verification

The core endpoint is POST /api/v1/verify. You send an email address, and the API returns a detailed verification result.

Request

POST https://api.emailkit.dev/api/v1/verify
Content-Type: application/json
Authorization: Bearer ek_your_api_key
Idempotency-Key: unique-request-id-123
{
  "email": "[email protected]"
}

The Idempotency-Key header is optional but recommended. It ensures that retrying the same request (due to network timeouts, for instance) does not deduct credits twice. Use a UUID or any unique string per request.

Response

A successful verification returns a JSON object with the verdict and detailed attributes:

{
  "email": "[email protected]",
  "status": "valid",
  "score": 0.95,
  "deliverability": "deliverable",
  "attributes": {
    "disposable": false,
    "freeProvider": true,
    "roleAccount": false,
    "catchAll": false,
    "mxRecordsFound": true,
    "smtpValid": true
  },
  "domainReputation": "high",
  "requestId": "req_abc123"
}

Here is what each field means:

Field Type Description
email string The email address that was verified
status string Verification result: valid, invalid, risky, or unknown
score number Confidence score from 0.0 to 1.0
deliverability string deliverable, undeliverable, or unknown
attributes.disposable boolean Whether the address uses a disposable/temporary email provider
attributes.freeProvider boolean Whether the domain is a free email provider (Gmail, Yahoo, etc.)
attributes.roleAccount boolean Whether the address is role-based (info@, admin@, support@)
attributes.catchAll boolean Whether the domain accepts mail for any address
attributes.mxRecordsFound boolean Whether the domain has valid MX records
attributes.smtpValid boolean Whether the SMTP handshake confirmed the mailbox exists
domainReputation string or null Domain reputation rating when available
requestId string Unique identifier for this request (useful for debugging)

Code Examples

cURL

curl -X POST https://api.emailkit.dev/api/v1/verify \
  -H "Authorization: Bearer ek_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"email": "[email protected]"}'

Node.js (fetch)

async function verifyEmail(email) {
  const response = await fetch("https://api.emailkit.dev/api/v1/verify", {
    method: "POST",
    headers: {
      "Authorization": "Bearer ek_your_api_key",
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ email }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Verification failed: ${error.error.code}`);
  }

  return response.json();
}

// Usage
const result = await verifyEmail("[email protected]");

if (result.deliverability === "deliverable") {
  console.log("Email is valid and deliverable");
} else if (result.attributes.disposable) {
  console.log("Disposable email detected");
} else {
  console.log(`Verification result: ${result.status}`);
}

Python (requests)

import requests
import uuid

def verify_email(email: str, api_key: str) -> dict:
    response = requests.post(
        "https://api.emailkit.dev/api/v1/verify",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={"email": email},
    )
    response.raise_for_status()
    return response.json()


# Usage
result = verify_email("[email protected]", "ek_your_api_key")

if result["deliverability"] == "deliverable":
    print("Email is valid and deliverable")
elif result["attributes"]["disposable"]:
    print("Disposable email detected")
else:
    print(f"Verification result: {result['status']}")

Bulk Verification

For verifying large lists, the bulk endpoint accepts an array of email addresses and processes them asynchronously. Small batches (50 or fewer) are processed synchronously and return results immediately. Larger jobs are queued and processed in the background.

Request:

curl -X POST https://api.emailkit.dev/api/v1/verify/bulk \
  -H "Authorization: Bearer ek_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "emails": [
      "[email protected]",
      "[email protected]",
      "[email protected]"
    ]
  }'

For jobs with more than 50 emails, the API returns a 202 Accepted response with a job ID:

{
  "id": "job_abc123",
  "status": "pending",
  "totalCount": 5000,
  "processedCount": 0,
  "progress": 0,
  "createdAt": "2026-02-28T10:00:00.000Z",
  "resultExpiresAt": "2026-03-14T10:00:00.000Z",
  "requestId": "req_xyz789"
}

Poll the job status with GET /api/v1/verify/bulk/:jobId, and once it reaches completed, download the results as CSV from GET /api/v1/verify/bulk/:jobId/results.

Checking Your Credit Balance

Before running large verification jobs, you may want to check your available credits:

curl https://api.emailkit.dev/api/v1/credits \
  -H "Authorization: Bearer ek_your_api_key"
{
  "balance": 48500,
  "updatedAt": "2026-02-28T10:30:00.000Z",
  "requestId": "req_def456"
}

Each single verification costs one credit. Non-billable results (addresses that could not be verified due to server errors, with a confidence score below 60%) are automatically refunded.

Rate Limiting

EmailKit enforces rate limits to ensure fair usage across all users. The current limits are:

  • Per-user: 50 requests per second
  • Global: 2,000 requests per second across all users

Every API response includes rate limit headers so your application can adapt:

X-RateLimit-Limit: 50
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1709114401

When you exceed the limit, the API returns a 429 Too Many Requests response with a Retry-After header:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. You can make 50 requests per second.",
    "retryAfter": 1
  },
  "requestId": "req_abc123"
}

Handling rate limits in practice. Use exponential backoff: wait 1 second on the first 429, then 2, then 4, capping at a maximum. Here is a Node.js implementation:

async function verifyWithRetry(email, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch("https://api.emailkit.dev/api/v1/verify", {
      method: "POST",
      headers: {
        "Authorization": "Bearer ek_your_api_key",
        "Content-Type": "application/json",
        "Idempotency-Key": crypto.randomUUID(),
      },
      body: JSON.stringify({ email }),
    });

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get("Retry-After") || "1");
      const backoff = retryAfter * Math.pow(2, attempt);
      await new Promise((resolve) => setTimeout(resolve, backoff * 1000));
      continue;
    }

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error.message);
    }

    return response.json();
  }

  throw new Error("Max retries exceeded");
}

Error Handling

The API uses standard HTTP status codes and returns structured error objects. Here are the error responses you should handle:

Status Code Meaning
401 AUTH_ERROR Missing or invalid API key
402 INSUFFICIENT_CREDITS Not enough credits to complete the verification
422 VALIDATION_ERROR Invalid request body (e.g., malformed email)
429 RATE_LIMIT_EXCEEDED Too many requests -- back off and retry
500 INTERNAL_ERROR Server error -- retry with idempotency key

All error responses follow this structure:

{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Insufficient credits to perform verification",
    "creditsRequired": 1,
    "creditsAvailable": 0
  },
  "requestId": "req_abc123"
}

The requestId field is always present in every response -- success or failure. Include it when contacting support, as it allows the team to trace the exact request in the system logs.

A note on idempotency. When you include an Idempotency-Key header, you can safely retry any failed request with the same key. If the original request succeeded but you didn't receive the response (network timeout, for example), the retry will return the cached response without deducting additional credits.

Webhook Integration

For asynchronous workflows, EmailKit supports webhooks that notify your application when verifications complete. This is particularly useful for bulk jobs where you don't want to poll for status.

Create a webhook via the API:

curl -X POST https://api.emailkit.dev/api/v1/webhooks \
  -H "Authorization: Bearer ek_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/emailkit",
    "events": ["verification.completed", "bulk.completed", "bulk.failed"]
  }'

When an event fires, EmailKit sends a POST request to your URL with the event payload. For single verifications, the payload includes the full verification result. For bulk jobs, it includes the job summary and a URL to download the results.

You can manage your webhooks -- list, update, and delete them -- through the same API. See the full API documentation for details on webhook event types and payload formats.

Common Integration Patterns

Pattern 1: Signup Form Validation

The most common use case is verifying emails during user registration. The verification call happens server-side, after the form is submitted but before the account is created.

// Express.js signup route
app.post("/signup", async (req, res) => {
  const { email, password, name } = req.body;

  // Step 1: Verify the email
  const verification = await verifyEmail(email);

  if (verification.deliverability === "undeliverable") {
    return res.status(400).json({
      error: "This email address appears to be invalid. Please check for typos.",
    });
  }

  if (verification.attributes.disposable) {
    return res.status(400).json({
      error: "Disposable email addresses are not allowed. Please use a permanent email.",
    });
  }

  // Step 2: Create the account (email is verified)
  const user = await createUser({ email, password, name });

  // Step 3: Send welcome email (confident it will be delivered)
  await sendWelcomeEmail(user.email);

  res.json({ success: true, userId: user.id });
});

Most applications block undeliverable results and disposable addresses outright, allow risky results (catch-all domains) with internal flagging, and let unknown results through with a scheduled re-verification.

Pattern 2: Checkout Email Confirmation

For e-commerce, an invalid email at checkout means the customer never receives their order confirmation, shipping notifications, or digital product delivery. Verifying at checkout is a direct revenue protection measure.

# Django checkout view
def process_checkout(request):
    email = request.POST.get("email")

    result = verify_email(email, settings.EMAILKIT_API_KEY)

    if result["deliverability"] == "undeliverable":
        return JsonResponse({
            "error": "We couldn't verify this email. Please use a different address."
        }, status=400)

    # Proceed with payment processing
    order = create_order(request.user, email=email)
    charge = process_payment(order)

    return JsonResponse({"order_id": order.id})

Pattern 3: CRM and List Import Sync

When importing contacts from a CSV, third-party integration, or CRM migration, use the bulk endpoint to verify the entire list before it enters your system.

import requests
import uuid
import time

def verify_and_import_contacts(emails: list, api_key: str):
    # Submit bulk verification job
    response = requests.post(
        "https://api.emailkit.dev/api/v1/verify/bulk",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={"emails": emails},
    )
    response.raise_for_status()
    job = response.json()

    # Poll until complete (for large lists)
    if job["status"] == "pending":
        while True:
            time.sleep(5)
            status_resp = requests.get(
                f"https://api.emailkit.dev/api/v1/verify/bulk/{job['id']}",
                headers={"Authorization": f"Bearer {api_key}"},
            )
            status = status_resp.json()
            print(f"Progress: {status['progress']}%")

            if status["status"] == "completed":
                break
            if status["status"] == "failed":
                raise Exception("Bulk job failed")

        # Download results CSV
        results_resp = requests.get(
            f"https://api.emailkit.dev/api/v1/verify/bulk/{job['id']}/results",
            headers={"Authorization": f"Bearer {api_key}"},
        )
        return results_resp.text  # CSV content

    # Small batch -- results returned inline
    return job["results"]

This pattern is especially valuable during platform migrations, where you don't want to carry years of email decay into your new system.

Pattern 4: Scheduled List Hygiene

Even with real-time verification at signup, email addresses decay over time. Set up a scheduled task that exports your active contact list, submits it to the bulk endpoint, and removes or flags addresses that come back as invalid or unknown. Monthly or quarterly is sufficient for most applications.

Best Practices

Always verify server-side. Client-side validation (JavaScript in the browser) is useful for instant feedback on obvious typos, but it can be bypassed. The authoritative verification call should always happen on your server.

Cache results when appropriate. If the same email is submitted multiple times in a short window (e.g., a user retrying a failed signup), you don't need to verify it again. Cache the result for 15 to 30 minutes. The idempotency key mechanism also helps here -- retrying with the same key returns the cached result at no extra cost.

Don't block on risky results. A risky status usually means the domain is a catch-all or the mail server was temporarily unavailable. These addresses may well be valid. Flag them internally, but don't prevent the user from proceeding.

Handle unknown gracefully. If the verification returns unknown (the mail server timed out or refused the connection), allow the user to proceed and schedule a re-verification. Some mail servers are intermittently unreachable, and a retry an hour later often succeeds.

Monitor your credit balance. Build in a check so that your application doesn't hit 402 Insufficient Credits during peak traffic. The GET /api/v1/credits endpoint is lightweight and rate-limit-friendly. Poll it periodically or after high-volume operations.

Use idempotency keys for retries. When retrying a request, include the same Idempotency-Key header to prevent double-charges. The header is optional, but recommended for any request you might retry due to network issues, load balancer replays, or client-side retry logic.

Respect rate limits proactively. Rather than hitting 429 errors and backing off, use the X-RateLimit-Remaining header to throttle your requests before hitting the limit. If you see remaining count approaching zero, slow down.

Getting Started

Integrating an email verification API is one of the highest-leverage changes you can make to your signup flow. A single HTTP call and a few lines of error handling protect every form submission, campaign send, and contact import from bad data.

Ready to integrate? Sign up for an EmailKit API key and start verifying emails in minutes. Your first verifications are free, and the API documentation covers every endpoint, parameter, and response field in detail.


Next up: Learn about Disposable Email Detection -- how to identify throwaway addresses and protect your signup flow from abuse.