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.