API Documentation
Complete reference for every endpoint, response field, error code, and webhook contract. Looking for the 5-minute quickstart?
What MailOdds Does
MailOdds tells you whether to accept, reject, or flag an email address -- before it enters your database, CRM, or mailing list.
Good use cases
- Validate at signup to block disposable and non-existent emails
- Clean legacy mailing lists before a campaign
- Reduce bounce rates and protect sender reputation
- Enforce compliance with suppression lists
Not designed for
- Re-validating the same email on every request (cache results instead)
- Blocking users solely because status is "catch_all" (use the
actionfield for decisioning) - Processing large lists via the real-time endpoint (use bulk jobs)
Choose Your Integration Path
Get your API key to start
Create a free account for 1,000 validations/month, or use an mo_test_ key with test domains to integrate without credits.
API Quick Reference
Base URL: https://api.mailodds.com
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/validate | Validate a single email address |
| POST | /v1/jobs | Create bulk validation job (JSON) |
| POST | /v1/jobs/upload | Create bulk job (file upload) |
| POST | /v1/jobs/upload/presigned | Get S3 presigned URL for large files |
| GET | /v1/jobs/:id | Get job status and progress |
| GET | /v1/jobs/:id/results | Get validation results (JSON/CSV/NDJSON) |
| GET | /v1/jobs/:id/stream | Stream job progress (SSE) |
| POST | /v1/jobs/:id/cancel | Cancel a running job |
| DELETE | /v1/jobs/:id | Delete a completed job |
| GET | /v1/suppression | List suppression entries |
| POST | /v1/suppression | Add suppression entry |
| DELETE | /v1/suppression/:id | Remove suppression entry |
| POST | /v1/suppression/import | Bulk import suppression list |
| POST | /v1/suppression/check | Check if email matches suppression list |
| GET | /v1/policies | List validation policies |
| POST | /v1/policies | Create validation policy |
| POST | /v1/policies/test | Test policy evaluation |
| GET | /v1/telemetry/summary | Get validation metrics for dashboards |
OpenAPI Specification
OpenAPI 3.2.0 compliant specification
Use our machine-readable OpenAPI specification to generate client libraries, import into API tools like Postman or Insomnia, or integrate with your existing development workflow.
Authentication
All API requests require authentication using a Bearer token. Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEYGetting Your API Key
Sign up for a free account to get your API key from the dashboard. Each account includes a generous free tier.
Test Mode
For integration testing, use API keys with the mo_test_ prefix
combined with special test domains that return predictable results without consuming credits.
Test Domains
| Email Domain | Result | Description |
|---|---|---|
*@deliverable.mailodds.com | valid / accept | Always deliverable |
*@invalid.mailodds.com | invalid / reject | Mailbox not found |
*@risky.mailodds.com | risky / accept_with_caution | Catch-all detected |
*@disposable.mailodds.com | invalid / reject | Disposable email |
*@role.mailodds.com | risky / accept_with_caution | Role account |
*@timeout.mailodds.com | unknown / retry_later | Simulates timeout |
*@freeprovider.mailodds.com | valid / accept | Free email provider (e.g. Gmail) |
Example
curl -X POST https://api.mailodds.com/v1/validate \
-H "Authorization: Bearer mo_test_YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "john@deliverable.mailodds.com"}'No Credits Consumed
Test domain responses include "test_mode": true and do not consume credits when used with mo_test_ keys.
Validating real emails (non-test domains) with test keys still consumes credits normally.
Validate Email Endpoint
/v1/validateRequest Body
{
"email": "user@example.com"
}Response (Valid Email)
{
"schema_version": "1.0",
"email": "user@example.com",
"status": "valid",
"action": "accept",
"sub_status": null,
"checks": {
"format": true,
"mx": true,
"smtp": true,
"disposable": false,
"role_account": false,
"catch_all": false
},
"domain": "example.com",
"mx_host": "mx1.example.com",
"processed_at": "2026-01-30T12:00:00Z"
}Response (Do Not Mail - Disposable)
{
"schema_version": "1.0",
"email": "user@temp-mail.com",
"status": "do_not_mail",
"action": "reject",
"sub_status": "disposable",
"checks": {
"format": true,
"mx": false,
"smtp": null,
"disposable": true,
"role_account": false,
"catch_all": null
},
"domain": "temp-mail.com",
"mx_host": null,
"processed_at": "2026-01-30T12:00:00Z"
}Response (Unknown - Retry Later)
{
"schema_version": "1.0",
"email": "user@slow-domain.com",
"status": "unknown",
"action": "retry_later",
"sub_status": "greylisted",
"retry_after_ms": 300000,
"checks": {
"format": true,
"mx": true,
"smtp": null,
"disposable": false,
"role_account": false,
"catch_all": null
},
"domain": "slow-domain.com",
"mx_host": "mx.slow-domain.com",
"processed_at": "2026-01-30T12:00:00Z"
}Response Fields
status enum - Primary validation result.
Values: valid, invalid, catch_all, unknown, do_not_mail. Factual result of the validation.
action enum - Recommended action for your application.
Values: accept, accept_with_caution, reject, retry_later. Branch on this field for decisioning. See Understanding Results.
sub_status string | null - Additional detail (informational only, do not branch on this).
Examples: format_invalid, mx_missing, disposable, role_account, greylisted.
retry_after_ms number | null - When action is retry_later, how long to wait before retrying (milliseconds).
checks object - Individual check results: format, mx, smtp, disposable, role_account, catch_all. Values are true, false, or null if not checked.
email string - The email address that was validated.
domain string - The domain part of the email address.
mx_host string | null - The primary MX host for the domain, if found.
processed_at string - ISO 8601 timestamp of when the validation was performed.
Try it live
Get your API key from the dashboard
Understanding Results
Read this before writing integration code
This section explains how to interpret validation responses correctly. Most integration mistakes come from misunderstanding these fields.
The Three Layers
Every validation response contains three layers of information:
status -- What we found (factual) valid, invalid, catch_all, unknown, do_not_mail
action -- What you should do (recommended) accept, accept_with_caution, reject, retry_later
sub_status -- Why (detail, informational) disposable, role_account, greylisted, mx_missing, etc.
Branch on action, not status. The action field accounts for context that status alone does not capture.
Decision Matrix
How to handle each action value
| action | What it means | What to do |
|---|---|---|
| accept | Email is deliverable | Allow signup / send mail |
| accept_with_caution | Deliverable but risky (catch-all, role account) | Allow signup, flag for review |
| reject | Not deliverable or disposable | Block signup / remove from list |
| retry_later | Temporary issue (greylisting, timeout) | Allow now, re-validate later. Check retry_after_ms. |
Example decision logic
const result = await validateEmail(email);
switch (result.action) {
case 'accept':
// Safe to proceed
allowSignup(email);
break;
case 'accept_with_caution':
// Allow but flag for manual review
allowSignup(email, { flagged: true });
break;
case 'reject':
// Block -- show user-friendly error
showError('Please use a valid email address.');
break;
case 'retry_later':
// Allow signup, re-validate in background
allowSignup(email);
scheduleRevalidation(email, result.retry_after_ms);
break;
}Sub-Status Reference
Common sub_status values and their meaning. These are informational -- branch on action instead.
| sub_status | Meaning | Typical action |
|---|---|---|
null | No additional detail | accept |
disposable | Temporary / throwaway email service | reject |
role_account | Generic address (info@, admin@, support@) | accept_with_caution |
catch_all_detected | Domain accepts all addresses | accept_with_caution |
greylisted | Mail server temporarily deferred verification | retry_later |
mx_missing | No mail server found for domain | reject |
format_invalid | Malformed email address | reject |
smtp_rejected | Mail server confirmed mailbox does not exist | reject |
smtp_unreachable | Could not connect to mail server | retry_later |
suppression_match | Matched your suppression list | reject |
Bulk Validation (Pro+)
Validate large lists of emails asynchronously. Upload a CSV file or submit a JSON array of up to 100,000 emails. Results are processed in the background and you'll be notified via webhook when complete.
/jobsCreate a bulk validation job from a JSON array of emails
Request Body
{
"emails": [
"user1@example.com",
"user2@example.com",
"user3@example.com"
],
"dedup": true, // optional - remove duplicates before processing (saves credits)
"callback_url": "https://your-server.com/webhook", // optional
"metadata": {"campaign": "newsletter"} // optional
}Save Credits with Deduplication
Set dedup: true to automatically remove duplicate emails before processing. This is case-insensitive and ensures you only pay for unique emails.
Idempotency
Include an Idempotency-Key header with a unique value (max 64 chars). If a request with the same key was made within 24 hours, the existing job is returned instead of creating a duplicate.
Response
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "pending",
"total_count": 3,
"created_at": "2026-01-30T12:00:00Z"
}
}/v1/jobs/uploadCreate a bulk validation job by uploading a CSV, Excel, or TXT file
Request (multipart/form-data)
curl -X POST https://api.mailodds.com/v1/jobs/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@emails.csv" \
-F "dedup=true" \
-F 'metadata={"campaign": "newsletter"}'Supported File Formats
CSV: First column should contain emails (header optional)
Excel (.xlsx, .xls): First column of first sheet should contain emails
ODS (OpenDocument): First column of first sheet should contain emails
TXT: One email per line
Max 100,000 emails per job.
Save Credits with Deduplication
Add -F "dedup=true" to remove duplicate emails before processing. This is case-insensitive and ensures you only pay for unique emails.
/v1/jobs/upload/presignedGet a presigned S3 URL for uploading large files (>10MB) directly to cloud storage
When to Use S3 Upload
For files larger than 10MB, use S3 presigned upload to avoid timeouts. This is a 3-step process: get presigned URL, upload to S3, then create the job.
Step 1: Get Presigned Upload Credentials
curl -X POST https://api.mailodds.com/v1/jobs/upload/presigned \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "large_list.csv", "content_type": "text/csv"}'Response
{
"schema_version": "1.0",
"upload": {
"url": "https://s3.amazonaws.com/mailodds-uploads",
"fields": {
"key": "uploads/abc123/large_list.csv",
"AWSAccessKeyId": "...",
"policy": "...",
"signature": "..."
},
"s3_key": "uploads/abc123/large_list.csv",
"expires_in": 3600
}
}Step 2: Upload File to S3
curl -X POST "${URL}" \
-F "key=${KEY}" \
-F "AWSAccessKeyId=${ACCESS_KEY_ID}" \
-F "policy=${POLICY}" \
-F "signature=${SIGNATURE}" \
-F "file=@large_list.csv"Step 3: Create Job from S3 File
Use the s3_key from Step 1 to create the validation job:
curl -X POST https://api.mailodds.com/v1/jobs/upload/s3 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"s3_key": "uploads/abc123/large_list.csv", "dedup": true}'Note: S3 upload is optional and only available when configured. Files are automatically deleted from S3 after job creation.
/v1/jobs/{job_id}Check the status and progress of a bulk validation job
Response (Processing)
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "processing",
"total_count": 1000,
"processed_count": 450,
"progress_percent": 45,
"created_at": "2026-01-30T12:00:00Z"
}
}Response (Completed)
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "completed",
"total_count": 1000,
"processed_count": 1000,
"progress_percent": 100,
"summary": {
"valid": 850,
"invalid": 80,
"catch_all": 40,
"unknown": 20,
"do_not_mail": 10
},
"created_at": "2026-01-30T12:00:00Z",
"completed_at": "2026-01-30T12:05:00Z"
}
}/v1/jobs/{job_id}/resultsDownload validation results as CSV or NDJSON
Query Parameters
format string - Output format: csv (default) or ndjson
filter string - Filter results: valid_only (deliverable emails) or invalid_only (undeliverable emails). Omit for all results.
dedup boolean - Set to true to remove duplicate emails from results (case-insensitive).
sort string - Sort order: sequence (upload order, default) or recent (newest first).
signed_url string - Return a pre-signed URL instead of direct download: csv or ndjson. URL expires after 1 hour.
Example: Download valid emails only (deduplicated)
curl "https://api.mailodds.com/v1/jobs/job_abc123xyz/results?format=csv&filter=valid_only&dedup=true" \
-H "Authorization: Bearer YOUR_API_KEY" \
-o valid_emails.csv/v1/jobs/{job_id}/streamSubscribe to real-time job progress updates via Server-Sent Events (SSE)
Authentication via Query Parameter
For EventSource compatibility, pass your API key as a token query parameter instead of the Authorization header.
Events
status Initial job state on connectprogress Processing update (every 100 emails)done Job completed, cancelled, or failederror Error occurredExample (JavaScript)
const eventSource = new EventSource(
'https://api.mailodds.com/v1/jobs/job_xyz789/stream?token=mo_live_xxx'
);
eventSource.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
console.log(`Progress: ${data.percent}%`);
});
eventSource.addEventListener('done', (e) => {
console.log('Job completed!');
eventSource.close();
});Webhooks
Get notified when jobs complete (Pro+ plans)
Configure a webhook URL in your dashboard settings or provide a callback_url when creating a job. We'll POST events when your job completes or fails.
Webhook Payload (job.completed)
{
"event": "job.completed",
"job": {
"id": "job_abc123xyz",
"status": "completed",
"total_count": 1000,
"summary": {
"valid": 850,
"invalid": 80,
"catch_all": 40,
"unknown": 20,
"do_not_mail": 10
}
},
"timestamp": "2026-01-30T12:05:00Z"
}Webhook Headers
X-MailOdds-Event Event type (job.completed, job.failed)X-MailOdds-Signature HMAC-SHA256 signature for verificationOperational Contract
| Delivery timeout | Your endpoint must respond within 10 seconds. Slower responses are treated as failures. |
| Retry policy | Failed deliveries (non-2xx or timeout) are retried 5 times with exponential backoff: 30s, 2m, 10m, 1h, 6h. |
| Success criteria | Any 2xx status code is treated as successful delivery. |
| Idempotency | Webhooks may be delivered more than once. Use the job.id to deduplicate. |
| IP allowlist | Webhooks originate from 65.108.88.163. Allowlist this IP if your firewall blocks inbound. |
Signature Verification
Always verify the X-MailOdds-Signature header to confirm the request is from MailOdds. Your signing secret is in Dashboard → Settings → Webhooks.
import { createHmac } from 'crypto';
function verifyWebhook(payload, signature, secret) {
const expected = createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === expected;
}
// Express example
app.post('/webhook', (req, res) => {
const sig = req.headers['x-mailodds-signature'];
if (!verifyWebhook(JSON.stringify(req.body), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process event...
res.status(200).send('ok');
});Job Statuses
Job created, waiting to start processing
Currently validating emails
All emails validated, results ready for download
Job encountered an error during processing
Suppression List API
Manage your suppression list to automatically block specific emails, domains, or patterns during validation. Emails matching your suppression list return status: "do_not_mail" with sub_status: "suppression_match".
/v1/suppressionList suppression entries with optional filtering and pagination
Query Parameters
type Filter by type: email, domain, or patternsearch Search by value or reasonpage, per_page Pagination (default: page=1, per_page=50)/v1/suppressionAdd one or more entries to your suppression list
Request Body
{
"entries": [
{
"type": "email",
"value": "spam@example.com",
"reason": "Unsubscribed"
},
{
"type": "domain",
"value": "competitor.com",
"reason": "Competitor domain"
},
{
"type": "pattern",
"value": ".*@tempmail\\..*",
"reason": "Temporary email pattern"
}
]
}Entry Types
email: Block a specific email address. domain: Block all emails from a domain. pattern: Block emails matching a regex pattern.
/v1/suppressionRemove entries from your suppression list
Request Body
{
"ids": [123, 456, 789]
}/v1/suppression/checkCheck if an email matches your suppression list without counting as a validation
Request Body
{
"email": "test@competitor.com"
}Response (Match Found)
{
"suppressed": true,
"match": {
"type": "domain",
"value": "competitor.com",
"reason": "Competitor domain"
}
}Suppression in Validation Response
When a validated email matches your suppression list
{
"email": "blocked@competitor.com",
"status": "do_not_mail",
"action": "reject",
"sub_status": "suppression_match",
"free_email": false,
"suppression": {
"match_type": "domain",
"match_value": "competitor.com",
"reason": "Competitor domain"
}
}Validation Policies API
Create rules to customize how validation results are interpreted. Override default actions based on status, domain, check results, or reason codes.
Plan Limits
Free plans: 1 policy, 3 rules max. Pro+ plans: Unlimited policies and rules.
Rule Types
| Type | Description | Example Condition |
|---|---|---|
status_override | Match by validation status | {"status": "catch_all"} |
domain_filter | Match by domain allowlist/blocklist | {"domain_mode": "blocklist", "domains": [...]} |
check_requirement | Require specific check to pass | {"check": "smtp", "required": true} |
sub_status_override | Match by reason code | {"sub_status": "role_account"} |
/v1/policiesList all validation policies for your account
Example
curl https://api.mailodds.com/v1/policies \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"policies": [
{
"id": 1,
"name": "Strict Policy",
"description": "Reject catch-all and role accounts",
"is_default": true,
"is_enabled": true,
"rule_count": 3
}
],
"limits": {
"max_policies": -1,
"max_rules_per_policy": -1,
"plan": "pro"
}
}/v1/policies/presetsGet available preset templates for quick policy creation
Available Presets
- strict - Reject catch-all, role accounts, and unknown status
- permissive - Accept catch-all and role accounts with caution
- smtp_required - Require SMTP verification to pass
/v1/policiesCreate a new validation policy with rules
Example
curl -X POST https://api.mailodds.com/v1/policies \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Strict Policy",
"description": "Reject catch-all emails",
"is_default": true,
"rules": [
{
"type": "status_override",
"condition": {"status": "catch_all"},
"action": {"action": "reject"}
}
]
}'Or Create from Preset
curl -X POST https://api.mailodds.com/v1/policies/from-preset \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"preset_id": "strict", "name": "My Strict Policy"}'/v1/policies/testTest how a policy would evaluate a validation result
Example
curl -X POST https://api.mailodds.com/v1/policies/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"policy_id": 1,
"test_result": {
"email": "test@catch-all-domain.com",
"status": "catch_all",
"action": "accept_with_caution"
}
}'Response
{
"original": {
"status": "catch_all",
"action": "accept_with_caution"
},
"modified": {
"status": "catch_all",
"action": "reject"
},
"matched_rule": {
"id": 1,
"type": "status_override",
"condition": {"status": "catch_all"}
},
"rules_evaluated": 3
}Policy Applied in Validation Response
When a policy modifies a validation result
{
"email": "info@example.com",
"status": "valid",
"action": "reject",
"policy_applied": {
"policy_id": 1,
"policy_name": "Strict Policy",
"rule_id": 3,
"rule_type": "sub_status_override"
}
}Job Management
Cancel or delete bulk validation jobs.
/v1/jobs/{job_id}/cancelCancel a pending or processing job. Partial results are preserved.
Example
curl -X POST https://api.mailodds.com/v1/jobs/job_abc123xyz/cancel \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "cancelled",
"total_count": 1000,
"processed_count": 450,
"cancelled_pending": 550, // emails not validated due to cancellation
"progress_percent": 45
}
}Partial Results
You can still download results for emails that were validated before cancellation. The cancelled_pending field shows how many emails were skipped.
/v1/jobs/{job_id}Permanently delete a completed or cancelled job and its results
Example
curl -X DELETE https://api.mailodds.com/v1/jobs/job_abc123xyz \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"schema_version": "1.0",
"deleted": true,
"job_id": "job_abc123xyz"
}Irreversible Action
Deletion is permanent. All validation results for this job will be removed and cannot be recovered. Running jobs cannot be deleted - cancel them first.
Telemetry
Monitor your validation metrics to build dashboards and track performance.
/v1/telemetry/summaryGet validation metrics for your account
Query Parameters
window string - Time window: 1h (last hour), 24h (default), or 30d (last 30 days)
group_by string - Group results by: api_key (breakdown per API key). Omit for aggregate totals.
Example
curl "https://api.mailodds.com/v1/telemetry/summary?window=24h" \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"schemaVersion": "1.0",
"window": "24h",
"generatedAt": "2026-01-31T12:00:00Z",
"timezone": "UTC",
"totals": {
"validations": 12500,
"creditsUsed": 12500
},
"rates": {
"deliverable": 0.872,
"reject": 0.084,
"unknown": 0.044,
"suppressed": 0.003
},
"topReasons": [
{"reason": "smtp_failed", "count": 450},
{"reason": "disposable", "count": 230}
],
"topDomains": [
{"domain": "gmail.com", "volume": 4500, "deliverable": 0.94}
]
}ETag Caching
This endpoint supports ETag-based caching. Include If-None-Match header with the previous ETag to receive 304 Not Modified when data hasn't changed.
Code Examples
cURL
curl -X POST https://api.mailodds.com/v1/validate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"email": "user@example.com"}'JavaScript (Fetch API)
const validateEmail = async (email) => {
const response = await fetch('https://api.mailodds.com/v1/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY'
},
body: JSON.stringify({ email })
});
if (!response.ok) {
if (response.status === 429) {
// Rate limited -- wait and retry
await new Promise(r => setTimeout(r, 2000));
return validateEmail(email);
}
if (response.status >= 500) {
// Server error -- fail open
return { action: 'accept', fallback: true };
}
throw new Error(`API error: ${response.status}`);
}
return response.json();
};
// Usage
const result = await validateEmail('user@example.com');
if (result.action === 'accept') {
console.log('Email is valid');
}Python (requests)
import requests
from time import sleep
def validate_email(email, max_retries=3):
url = 'https://api.mailodds.com/v1/validate'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY'
}
for attempt in range(max_retries):
try:
resp = requests.post(url, json={'email': email}, headers=headers, timeout=10)
if resp.status_code == 200:
return resp.json()
if resp.status_code in (429, 500, 502, 503):
sleep(2 ** attempt)
continue
return {'error': resp.status_code}
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
continue
# Fail open: allow signup, validate later
return {'action': 'accept', 'fallback': True}
# Usage
result = validate_email('user@example.com')
if result.get('action') == 'accept':
print('Email is valid')PHP
<?php
function validateEmail($email) {
$ch = curl_init('https://api.mailodds.com/v1/validate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'email' => $email
]));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Usage
$result = validateEmail('user@example.com');
print_r($result);
?>Errors & Retry
The API uses standard HTTP status codes to indicate success or failure:
OK
Request was successful
Bad Request
Invalid request format or missing required fields
Unauthorized
Missing or invalid API key
Payment Required
Insufficient credits. The response includes credits_available, credits_needed, and upgrade_url so you can handle this programmatically.
Forbidden
Feature not available on your plan (e.g., batch validation requires Pro+)
Too Many Requests
Rate limit exceeded
Internal Server Error
Something went wrong on our end
Recommended Retry Pattern
async function validateWithRetry(email, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const resp = await fetch('https://api.mailodds.com/v1/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(10000)
});
if (resp.ok) return await resp.json();
if (resp.status === 429 || resp.status >= 500) {
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
continue; // retry
}
// 400, 401, 402, 403 -- do not retry
return { error: resp.status, body: await resp.json() };
} catch (e) {
if (attempt < maxRetries - 1) continue;
// All retries failed -- fail open
return { action: 'accept', fallback: true };
}
}
// All retries exhausted -- fail open
return { action: 'accept', fallback: true };
}Never block signups because MailOdds is unreachable
If the API times out or returns 5xx after retries, allow the signup and re-validate the email later. Your conversion matters more than a single validation check. This is called the "fail open" pattern.
402 Response Body
When you receive a 402, the response body contains everything you need to handle it programmatically:
{
"error": "insufficient_credits",
"message": "Not enough credits to validate 500 emails...",
"credits_available": 12,
"credits_needed": 500,
"current_plan": "starter",
"upgrade_url": "https://mailodds.com/dashboard/billing",
"recommended_action": "purchase_credits"
}Scaling & Best Practices
Do
- Cache validation results. An email validated today does not need re-validation tomorrow. Cache for 30-90 days.
- Use bulk jobs for lists of 50+ emails. More cost-efficient and does not consume rate limit.
- Use webhooks (Pro+) to get notified when bulk jobs finish instead of polling.
- Set
dedup: trueon bulk jobs to avoid paying for duplicate emails. - Use idempotency keys on bulk job creation to prevent duplicate jobs on retries.
Do Not
- Validate the same email on every page load. Validate once at signup, then cache the result.
- Use
/v1/validatefor CSV imports. Use /v1/jobs/upload instead. - Block your UI on a third-party API call. Run validation async when possible.
- Ignore
accept_with_cautionresults. These are legitimate emails that deserve extra scrutiny, not rejection.
Production Checklist
Before you go live
Review these items before deploying to production
API key stored in environment variable
Never hardcode API keys in source code
Error handling implemented
Retry on 429/5xx, fail open on timeout
Decision logic branches on action
Not just status -- see Understanding Results
Rate limits understood for your plan
Check rate limits and handle 429 responses
Bulk jobs used for list processing
Not the real-time /v1/validate endpoint
Test mode verified with mo_test_ key
Use test domains before switching to live key
Caching strategy in place
Avoid re-validating known emails -- cache results for 30-90 days
Rate Limits
Rate limits vary by plan to ensure fair usage and service quality:
Free
50
validations/month
10/sec
Starter
5,000
validations/month
50/sec
Pro
25,000
validations/month
100/sec
Business
100,000
validations/month
200/sec
Rate limit headers are included in all API responses to help you track your usage. Need more? See Enterprise plans.
Ready to Get Started?
Sign up for a free account and start validating emails in minutes
Create Free Account