MailOdds

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 action field for decisioning)
  • Processing large lists via the real-time endpoint (use bulk jobs)

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

Download YAML

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.

Auto-generate SDKs
Import to Postman
API documentation tools

Authentication

All API requests require authentication using a Bearer token. Include your API key in the Authorization header:

Authorization: Bearer YOUR_API_KEY

Getting 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

POST /v1/validate

Request 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:

1
status -- What we found (factual)

valid, invalid, catch_all, unknown, do_not_mail

2
action -- What you should do (recommended)

accept, accept_with_caution, reject, retry_later

3
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.

POST /jobs

Create 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"
  }
}
POST /v1/jobs/upload

Create 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.

POST /v1/jobs/upload/presigned

Get 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.

GET /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"
  }
}
GET /v1/jobs/{job_id}/results

Download 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
GET /v1/jobs/{job_id}/stream

Subscribe 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 connect
progress Processing update (every 100 emails)
done Job completed, cancelled, or failed
error Error occurred

Example (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 verification

Operational 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

pending

Job created, waiting to start processing

processing

Currently validating emails

completed

All emails validated, results ready for download

failed

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".

GET /v1/suppression

List suppression entries with optional filtering and pagination

Query Parameters

type Filter by type: email, domain, or pattern
search Search by value or reason
page, per_page Pagination (default: page=1, per_page=50)
POST /v1/suppression

Add 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.

DELETE /v1/suppression

Remove entries from your suppression list

Request Body

{
  "ids": [123, 456, 789]
}
POST /v1/suppression/check

Check 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"}
GET /v1/policies

List 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"
  }
}
GET /v1/policies/presets

Get 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
POST /v1/policies

Create 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"}'
POST /v1/policies/test

Test 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.

POST /v1/jobs/{job_id}/cancel

Cancel 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.

DELETE /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.

GET /v1/telemetry/summary

Get 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:

200

OK

Request was successful

400

Bad Request

Invalid request format or missing required fields

401

Unauthorized

Missing or invalid API key

402

Payment Required

Insufficient credits. The response includes credits_available, credits_needed, and upgrade_url so you can handle this programmatically.

403

Forbidden

Feature not available on your plan (e.g., batch validation requires Pro+)

429

Too Many Requests

Rate limit exceeded

500

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: true on 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/validate for CSV imports. Use /v1/jobs/upload instead.
  • Block your UI on a third-party API call. Run validation async when possible.
  • Ignore accept_with_caution results. 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