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 is an email platform with three APIs: validate addresses before they enter your database, send transactional and campaign emails with built-in validation, and manage subscriber lists with double opt-in. One API key, one dashboard.

Good use cases

  • Validate at signup to block disposable and non-existent emails
  • Clean legacy mailing lists before a campaign
  • Send transactional emails (receipts, resets) with pre-send validation
  • Build subscriber lists with double opt-in and suppression enforcement
  • Reduce bounce rates and protect sender reputation

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)
  • Sending to unvalidated recipient lists (enable validate_first or bulk-validate before sending)
  • Importing subscribers without double opt-in (consent protects deliverability and compliance)

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

MethodEndpointDescription
Validation
POST/v1/validateValidate a single email address
POST/v1/validate/batchValidate up to 50 emails in one request
POST/v1/jobsCreate bulk validation job (JSON)
POST/v1/jobs/uploadCreate bulk job (file upload)
POST/v1/jobs/upload/presignedGet S3 presigned URL for large files
GET/v1/jobs/:idGet job status and progress
GET/v1/jobs/:id/resultsGet validation results (JSON/CSV/NDJSON)
GET/v1/jobs/:id/streamStream job progress (SSE)
POST/v1/jobs/:id/cancelCancel a running job
DELETE/v1/jobs/:idDelete a completed job
Suppression
GET/v1/suppressionList suppression entries
POST/v1/suppressionAdd suppression entry
DELETE/v1/suppression/:idRemove suppression entry
POST/v1/suppression/importBulk import suppression list
POST/v1/suppression/checkCheck if email matches suppression list
GET/v1/suppression/statsGet suppression list statistics
Policies
GET/v1/policiesList validation policies
POST/v1/policiesCreate validation policy
POST/v1/policies/testTest policy evaluation
Sending Domains
POST/v1/sending-domainsRegister a sending domain
GET/v1/sending-domainsList sending domains
GET/v1/sending-domains/:idGet domain details and DNS records
DELETE/v1/sending-domains/:idRemove a sending domain
POST/v1/sending-domains/:id/verifyTrigger DNS verification
GET/v1/sending-domains/:id/identity-scoreGet domain identity score
Email Sending
POST/v1/deliverSend a single email
POST/v1/deliver/batchSend to multiple recipients
GET/v1/sending-statsGet sending statistics
Subscriber Lists
POST/v1/listsCreate a subscriber list
GET/v1/listsList all subscriber lists
GET/v1/lists/:idGet list details and subscriber count
DELETE/v1/lists/:idDelete a subscriber list
POST/v1/subscribe/:list_idSubscribe an email to a list
GET/v1/confirm/:tokenConfirm double opt-in subscription
Campaigns
POST/v1/campaignsCreate a campaign
POST/v1/campaigns/:id/scheduleSchedule a campaign
POST/v1/campaigns/:id/sendSend campaign immediately
POST/v1/campaigns/:id/cancelCancel a scheduled or sending campaign
POST/v1/campaigns/:id/variantsCreate A/B test variant
GET/v1/campaigns/:id/ab-resultsGet A/B test results
GET/v1/campaigns/:id/funnelGet campaign funnel metrics
GET/v1/campaigns/:id/delivery-confidenceGet delivery confidence score
OAuth 2.0
POST/oauth/tokenExchange credentials for an access token
POST/oauth/revokeRevoke an access or refresh token
POST/oauth/introspectIntrospect a token (check validity)
GET/.well-known/oauth-authorization-serverOAuth server metadata (RFC 8414)
GET/.well-known/jwks.jsonJSON Web Key Set (public keys)
Store Connections
GET/v1/storesList all store connections
POST/v1/storesConnect a new store
GET/v1/stores/:idGet store details and sync status
DELETE/v1/stores/:idDisconnect a store
POST/v1/stores/:id/syncTrigger a manual product sync
GET/v1/store-productsQuery synced products
GET/v1/store-products/:idGet a single product
Deliverability
GET/v1/sender-healthGet sender health score
POST/v1/bounce-analysesStart bounce analysis
GET/v1/bounce-analyses/:idGet bounce analysis results
GET/v1/bounce-analyses/:id/recordsGet individual bounce records
Telemetry
GET/v1/telemetry/summaryGet 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
Postman Collection
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 DomainResultDescription
*@deliverable.mailodds.comvalid / acceptAlways deliverable
*@invalid.mailodds.cominvalid / rejectMailbox not found
*@risky.mailodds.comcatch_all / accept_with_cautionCatch-all detected
*@disposable.mailodds.comdo_not_mail / rejectDisposable email
*@role.mailodds.comdo_not_mail / rejectRole account
*@timeout.mailodds.comunknown / retry_laterSimulates timeout
*@freeprovider.mailodds.comvalid / acceptFree 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 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",
  "domain": "example.com",
  "mx_found": true,
  "mx_host": "mx1.example.com",
  "smtp_check": true,
  "catch_all": false,
  "disposable": false,
  "role_account": false,
  "free_provider": false,
  "has_spf": true,
  "has_dmarc": true,
  "dmarc_policy": "reject",
  "dnsbl_listed": false,
  "depth": "enhanced",
  "processed_at": "2026-02-08T12: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",
  "domain": "temp-mail.com",
  "mx_found": false,
  "disposable": true,
  "role_account": false,
  "free_provider": false,
  "depth": "enhanced",
  "processed_at": "2026-02-08T12: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,
  "domain": "slow-domain.com",
  "mx_found": true,
  "mx_host": "mx.slow-domain.com",
  "disposable": false,
  "role_account": false,
  "free_provider": false,
  "depth": "enhanced",
  "processed_at": "2026-02-08T12: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, domain_not_found.

retry_after_ms

number | null - When action is retry_later, how long to wait before retrying (milliseconds).

email

string - The email address that was validated.

domain

string - The domain part of the email address.

mx_found

boolean - Whether MX records were found for the domain.

mx_host

string - The primary MX hostname. Omitted when MX not resolved.

smtp_check

boolean - Whether SMTP verification passed. Omitted when SMTP not checked.

catch_all

boolean - Whether domain is a catch-all. Omitted when SMTP not checked.

disposable

boolean - Whether the domain is a known disposable email provider.

role_account

boolean - Whether the address is a role account (e.g., info@, admin@).

free_provider

boolean - Whether the domain is a known free email provider (e.g., gmail.com, outlook.com).

has_spf

boolean - Whether the domain publishes an SPF record. Omitted for standard depth.

has_dmarc

boolean - Whether the domain publishes a DMARC record. Omitted for standard depth.

dmarc_policy

string - DMARC policy: none, quarantine, or reject. Omitted when no DMARC record found.

dnsbl_listed

boolean - Whether the domain's MX IP is on a DNS blocklist (Spamhaus ZEN). Omitted for standard depth.

depth

string - Validation depth used: standard (no SMTP) or enhanced (full SMTP, default).

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, domain_not_found, 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

actionWhat it meansWhat to do
acceptEmail is deliverableAllow signup / send mail
accept_with_cautionDeliverable but risky (catch-all, role account)Allow signup, flag for review
rejectNot deliverable or disposableBlock signup / remove from list
retry_laterTemporary 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_statusMeaningTypical action
nullNo additional detailaccept
disposableTemporary / throwaway email servicereject
role_accountGeneric address (info@, admin@, support@)accept_with_caution
catch_all_detectedDomain accepts all addressesaccept_with_caution
greylistedMail server temporarily deferred verificationretry_later
mx_missingNo mail server found for domainreject
format_invalidMalformed email addressreject
smtp_rejectedMail server confirmed mailbox does not existreject
smtp_unreachableCould not connect to mail serverretry_later
mx_timeoutDNS lookup timed out for domainretry_later
domain_not_foundDomain does not exist (NXDOMAIN)reject
suppression_matchMatched your suppression listreject
restricted_militaryMilitary domain restricted by compliance policyreject
restricted_sanctionedDomain restricted by sanctions/export controlsreject

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 (e.g., job.completed, message.delivered)
X-MailOdds-Signature HMAC-SHA256 signature for verification
X-Request-Id Unique request identifier for tracing
User-Agent MailOdds-Webhook/1.0

Operational Contract

Delivery timeoutYour endpoint must respond within 10 seconds. Slower responses are treated as failures.
Retry policyFailed deliveries (non-2xx or timeout) are retried 3 times with exponential backoff: 10s, 60s, 300s.
Success criteriaAny 2xx status code is treated as successful delivery.
IdempotencyWebhooks may be delivered more than once. Use the job.id to deduplicate.
IP allowlistWebhooks originate from 173.212.231.30. 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

TypeDescriptionExample Condition
status_overrideMatch by validation status{"status": "catch_all"}
domain_filterMatch by domain allowlist/blocklist{"domain_mode": "blocklist", "domains": [...]}
check_requirementRequire specific check to pass{"check": "smtp", "required": true}
sub_status_overrideMatch 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);
?>

Sending Domains (Growth+)

Configure and verify domains for authenticated email sending. MailOdds uses NS subdomain delegation so DKIM, SPF, and return-path records are managed automatically.

POST /v1/sending-domains

Register a new sending domain

Request

curl -X POST https://api.mailodds.com/v1/sending-domains \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "yourdomain.com"}'

Response (201)

{
  "domain": {
    "id": "d8f3a1b2-4c5e-6f7a-8b9c-0d1e2f3a4b5c",
    "domain": "yourdomain.com",
    "domain_type": "ns_delegated",
    "status": "pending_dns",
    "dkim_selector": "mo1",
    "dns_records": {
      "ns": {
        "type": "NS",
        "host": "mo.yourdomain.com",
        "targets": [
          "ns1.mailodds.net",
          "ns2.mailodds.net",
          "ns3.mailodds.net",
          "ns4.mailodds.net"
        ],
        "status": "pending",
        "verified_at": null
      }
    },
    "created_at": "2026-02-25T12:00:00Z",
    "updated_at": "2026-02-25T12:00:00Z"
  }
}

DNS Records Explained

MailOdds uses NS subdomain delegation. You add a single set of NS records, and MailOdds manages all authentication records under that subdomain.

RecordYou ConfigureMailOdds Manages
NSmo.yourdomain.com NS records pointing to ns1-ns4.mailodds.net-
DKIM-Auto-created under mo.yourdomain.com
SPF-Auto-created under mo.yourdomain.com
Return Path-Auto-created under mo.yourdomain.com
DMARCYou manage DMARC on your root domain-

Domain Status Values

StatusDescription
pending_dnsWaiting for NS records to be configured
dns_partialSome records verified, others still pending
activeAll records verified, ready to send
suspendedDomain suspended (DNS records removed or policy violation)
POST /v1/sending-domains/{id}/verify

Trigger DNS verification for a domain

Request

curl -X POST https://api.mailodds.com/v1/sending-domains/{domain_id}/verify \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

Returns the updated domain object with current record statuses. When all NS records resolve correctly, MailOdds auto-provisions DKIM, SPF, and return-path records and transitions the domain to active.

GET /v1/sending-domains/{id}/identity-score

Get a composite DNS health score for a domain

Request

curl https://api.mailodds.com/v1/sending-domains/{domain_id}/identity-score \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "identity_score": {
    "overall_score": 95,
    "checks": {
      "dkim":        { "status": "pass", "score": 20 },
      "spf":         { "status": "pass", "score": 20 },
      "dmarc":       { "status": "pass", "score": 25, "policy": "reject" },
      "mx":          { "status": "pass", "score": 15 },
      "return_path": { "status": "pass", "score": 15 }
    }
  }
}

Score is 0-100. Individual checks contribute: DKIM (20), SPF (20), DMARC (25), MX (15), Return Path (15). A reject DMARC policy scores highest.

Other Domain Endpoints

MethodEndpointDescription
GET/v1/sending-domainsList all sending domains for the account
GET/v1/sending-domains/{id}Get a single domain with DNS record statuses
DELETE/v1/sending-domains/{id}Permanently remove a domain and its DNS records

Email Sending (Growth+)

Send transactional emails with built-in validation, DKIM signing, and delivery tracking. Requires a verified sending domain.

POST /v1/deliver

Send a transactional email

Request

curl -X POST https://api.mailodds.com/v1/deliver \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [{"email": "recipient@example.com", "name": "Recipient"}],
    "from": "sender@yourdomain.com",
    "subject": "Order Confirmation",
    "html": "<h1>Order confirmed</h1>",
    "domain_id": "your-domain-uuid"
  }'

Request Body

FieldTypeRequiredDescription
toobject[]YesRecipients. Each object: email (required), name (optional)
fromstringYesSender address (must match sending domain)
subjectstringYesEmail subject line
domain_idstringYesSending domain UUID
htmlstring*HTML body (html or text required)
textstring*Plain text body (html or text required)
reply_tostringNoReply-to address
headersobjectNoCustom email headers
tagsstring[]NoTags for filtering in stats/webhooks
campaign_typestringNoAuto-generate JSON-LD from schema_data. Values: order_confirmation, shipping_notification, subscription_confirm, review_request, event_invitation, promotional, welcome, password_reset, appointment_reminder, ticket_confirmation
structured_dataobject | array | stringNoExplicit JSON-LD structured data (max 10KB). Takes priority over campaign_type auto-generation.
schema_dataobjectNoKey-value pairs for campaign_type JSON-LD resolution (e.g., order_number, tracking_url)
auto_detect_schemabooleanNoAuto-detect JSON-LD type from subject line (default: false)
ai_summarystringNoHidden text summary for AI email assistants (max 500 characters)
options.validate_firstbooleanNoPre-validate recipients (default: true)
options.tracking_domainstringNoOverride tracking domain

Response (202)

{
  "message_id": "msg_abc123xyz",
  "status": "queued",
  "recipients": 1,
  "suppressed": [],
  "validation": {
    "checked": true,
    "results": {}
  },
  "delivery": {
    "pool": "standard",
    "lane": "green",
    "queued_at": "2026-02-25T12:00:00+00:00"
  },
  "content_scan": {
    "action": "allow",
    "score": 0.1
  }
}
POST /v1/deliver/batch

Send to multiple recipients (max 100) with a shared message body

Request

curl -X POST https://api.mailodds.com/v1/deliver/batch \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["alice@example.com", "bob@example.com"],
    "from": "sender@yourdomain.com",
    "subject": "Weekly Update",
    "html": "<h1>This week at Acme</h1>",
    "domain_id": "your-domain-uuid"
  }'

Response (202)

{
  "total": 2,
  "accepted": 2,
  "rejected": [],
  "status": "queued",
  "delivery": {
    "pool": "standard",
    "lane": "green",
    "queued_at": "2026-02-25T12:00:00+00:00"
  }
}

Maximum 100 recipients per batch. Each recipient is individually checked against suppression lists and validation. If all recipients are rejected, status is "all_rejected" and no delivery object is returned.

GET /v1/sending-stats

Get aggregate sending statistics

Request

curl "https://api.mailodds.com/v1/sending-stats?period=30d" \
  -H "Authorization: Bearer YOUR_API_KEY"

Query parameter period: 7d (default), 30d, or 90d. Optionally filter by domain_id.

Response (200)

{
  "stats": {
    "period": "30d",
    "sent": 12500,
    "delivered": 12350,
    "bounced": 120,
    "deferred": 25,
    "failed": 5,
    "opened_total": 8200,
    "opened_unique": 6100,
    "clicked_total": 1500,
    "clicked_unique": 980,
    "unsubscribed": 45,
    "delivery_rate": 98.8,
    "open_rate": 49.4,
    "click_rate": 7.9,
    "bot_opens": 1200,
    "human_opens": 7000,
    "bot_open_pct": 14.6
  }
}

Idempotency

Include an Idempotency-Key header to safely retry deliver requests without sending duplicate emails.

  • Use a unique value per request (e.g., a UUID)
  • Same key within 24 hours returns the original response
  • Keys are scoped per account (different accounts can use the same key)
  • The header is optional. Omitting it means no idempotency protection.
curl -X POST https://api.mailodds.com/v1/deliver \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{...}'

Sending Rate Limits

EndpointLimit
POST /v1/deliver10 requests/second per account
POST /v1/deliver/batch5 requests/second per account
POST /v1/subscribe/:id10/min per IP, 1,000/hour per account

Exceeding limits returns 429 with a Retry-After header.

Sending Webhook Events

Configure webhooks to receive real-time delivery status updates.

EventDescription
message.queuedEmail accepted and queued for delivery
message.deliveredEmail delivered to recipient
message.bouncedEmail bounced (hard or soft)
message.deferredDelivery temporarily deferred
message.failedDelivery permanently failed
message.openedRecipient opened the email
message.clickedRecipient clicked a link

Delivery Event Payload

Delivery events include detailed SMTP information. Fields like smtp_code, bounce_type, and mx_host are omitted when empty.

{
  "event": "message.delivered",
  "message_id": "d3f1a2b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "account_id": 42,
  "domain_id": "78cdceaf-8dec-4a46-8884-b33f1bf80e3e",
  "to": "user@example.com",
  "status": "delivered",
  "mx_host": "aspmx.l.google.com",
  "attempts": 1,
  "timestamp": "2026-03-01T12:05:00Z"
}

Bounce Event Payload

{
  "event": "message.bounced",
  "message_id": "d3f1a2b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "account_id": 42,
  "domain_id": "78cdceaf-8dec-4a46-8884-b33f1bf80e3e",
  "to": "bad@example.com",
  "status": "bounced",
  "bounce_type": "hard",
  "smtp_code": 550,
  "smtp_response": "5.1.1 The email account does not exist",
  "enhanced_status_code": "5.1.1",
  "mx_host": "aspmx.l.google.com",
  "attempts": 1,
  "timestamp": "2026-03-01T12:05:00Z"
}

Open/Click Event Payload

Tracking events include client information and bot detection.

{
  "event": "message.clicked",
  "message_id": "d3f1a2b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "link_url": "https://example.com/offer",
  "link_index": 0,
  "ip_address": "203.0.113.42",
  "user_agent": "Mozilla/5.0 ...",
  "is_bot": false,
  "timestamp": "2026-03-01T14:30:00Z"
}

Subscriber Lists (Growth+)

Manage subscriber lists with built-in double opt-in confirmation, consent tracking, and email pre-validation.

POST /v1/lists

Create a subscriber list

Request

curl -X POST https://api.mailodds.com/v1/lists \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter",
    "description": "Weekly product updates",
    "confirmation_redirect_url": "https://example.com/subscribed"
  }'

Response (201)

{
  "list": {
    "id": "lst_abc123",
    "name": "Newsletter",
    "description": "Weekly product updates",
    "confirmation_redirect_url": "https://example.com/subscribed",
    "subscriber_count": 0,
    "confirmed_count": 0,
    "created_at": "2026-02-25T12:00:00Z",
    "updated_at": "2026-02-25T12:00:00Z"
  }
}

Plan limits: Growth (1 list), Pro (5 lists), Business and Enterprise (unlimited).

POST /v1/subscribe/{list_id}

Add a subscriber and send confirmation email

Request

curl -X POST https://api.mailodds.com/v1/subscribe/{list_id} \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "name": "Jane Doe",
    "page_url": "https://example.com/signup",
    "form_id": "footer-form"
  }'

Response (201)

{
  "subscriber": {
    "id": "sub_xyz789",
    "list_id": "lst_abc123",
    "email": "user@example.com",
    "name": "Jane Doe",
    "status": "pending",
    "consent_page_url": "https://example.com/signup",
    "consent_form_id": "footer-form",
    "consent_timestamp": "2026-02-25T12:00:00Z",
    "validation_result": {
      "status": "valid",
      "action": "accept"
    },
    "created_at": "2026-02-25T12:00:00Z"
  }
}

How It Works

  1. Email is pre-validated (invalid addresses are rejected immediately)
  2. Subscriber is created with status pending
  3. Confirmation email is sent with a unique token link
  4. When the subscriber clicks the link (GET /v1/confirm/{token}), status changes to confirmed
  5. Confirmed subscribers enter the green lane for faster delivery

Confirmation tokens expire after 72 hours. Plan limits: Growth (5,000 subscribers), Pro (25,000), Business (100,000), Enterprise (unlimited).

Other List Endpoints

MethodEndpointDescription
GET/v1/listsList all subscriber lists (paginated)
GET/v1/lists/{id}Get list details with subscriber counts
DELETE/v1/lists/{id}Delete a list (soft-delete)
GET/v1/lists/{id}/subscribersList subscribers (filter by status: pending, confirmed, unsubscribed, bounced)
DELETE/v1/lists/{id}/subscribers/{sub_id}Unsubscribe (retains consent record)
GET/v1/confirm/{token}Confirm subscription (public, no auth required)

Campaigns (Growth+)

Create, schedule, and send email campaigns to your contact lists. Every recipient is validated before delivery. Requires a verified sending domain and at least one contact list.

Campaign Lifecycle

Create a campaign, then schedule it for a future time or send it immediately. Cancel at any point before delivery completes.

Create a Campaign

POST /v1/campaigns
curl -X POST https://api.mailodds.com/v1/campaigns \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "March Newsletter",
    "subject": "What is new this month",
    "from_email": "news@yourdomain.com",
    "html_body": "<h1>March Update</h1><p>Here is what happened...</p>",
    "contact_list_id": "cl_abc123"
  }'

Response (201)

{
  "id": "camp_7f3a1b2c4d5e",
  "name": "March Newsletter",
  "subject": "What is new this month",
  "from_email": "news@yourdomain.com",
  "status": "draft",
  "contact_list_id": "cl_abc123",
  "created_at": "2026-03-12T14:00:00Z"
}

Schedule a Campaign

POST /v1/campaigns/{id}/schedule
curl -X POST https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/schedule \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"send_at": "2026-03-15T10:00:00Z"}'

Send Immediately

POST /v1/campaigns/{id}/send
curl -X POST https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/send \
  -H "Authorization: Bearer YOUR_API_KEY"

Cancel a Campaign

POST /v1/campaigns/{id}/cancel
curl -X POST https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/cancel \
  -H "Authorization: Bearer YOUR_API_KEY"

Cancellation stops any emails that have not yet been delivered. Emails already sent are unaffected.

Campaign Status Values

StatusDescription
draftCampaign created, not yet sent or scheduled
scheduledQueued for delivery at the specified send_at time
sendingDelivery is in progress
sentAll emails delivered successfully
cancelledCampaign was cancelled before delivery completed

A/B Testing

Create subject line or content variants and let MailOdds determine the winner based on statistical significance.

Create a Variant

POST /v1/campaigns/{id}/variants
curl -X POST https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/variants \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "You will love what is new this month",
    "html_body": "<h1>March Update</h1><p>Big changes ahead...</p>",
    "weight": 50
  }'

The weight field controls traffic distribution as a percentage (0 to 100). Weights across all variants should sum to 100.

Get A/B Results

GET /v1/campaigns/{id}/ab-results
curl https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/ab-results \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "campaign_id": "camp_7f3a1b2c4d5e",
  "variants": [
    {
      "variant_id": "var_001",
      "subject": "What is new this month",
      "sent": 5000,
      "opens": 1250,
      "clicks": 380,
      "open_rate": 25.0,
      "click_rate": 7.6,
      "is_winner": false,
      "statistical_significance": 0.82
    },
    {
      "variant_id": "var_002",
      "subject": "You will love what is new this month",
      "sent": 5000,
      "opens": 1650,
      "clicks": 510,
      "open_rate": 33.0,
      "click_rate": 10.2,
      "is_winner": true,
      "statistical_significance": 0.97
    }
  ]
}

A variant is marked is_winner: true when its statistical_significance exceeds 0.95 (95% confidence). Until that threshold is reached, no winner is declared.

Campaign Analytics

Track campaign performance from send through conversion with funnel metrics and delivery confidence scoring.

Campaign Funnel

GET /v1/campaigns/{id}/funnel
curl https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/funnel \
  -H "Authorization: Bearer YOUR_API_KEY"

Funnel Response (200)

{
  "campaign_id": "camp_7f3a1b2c4d5e",
  "sent": 10000,
  "delivered": 9850,
  "opened": 2950,
  "clicked": 890,
  "converted": 142,
  "unsubscribed": 23,
  "bounced": 120,
  "complained": 7
}

Delivery Confidence

GET /v1/campaigns/{id}/delivery-confidence
curl https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/delivery-confidence \
  -H "Authorization: Bearer YOUR_API_KEY"

Confidence Response (200)

{
  "campaign_id": "camp_7f3a1b2c4d5e",
  "confidence_score": 87,
  "factors": [
    {
      "name": "list_hygiene",
      "impact": "positive",
      "recommendation": "Contact list was validated within the last 30 days"
    },
    {
      "name": "sender_reputation",
      "impact": "positive",
      "recommendation": "Domain has strong authentication (DKIM, SPF, DMARC)"
    },
    {
      "name": "content_quality",
      "impact": "neutral",
      "recommendation": "Consider adding a plain text alternative for better deliverability"
    }
  ]
}

OAuth 2.0 (Growth+)

Register third-party applications and issue scoped access tokens using standard OAuth 2.0 flows. Supports authorization code with PKCE, client credentials, and refresh token grants.

POST /oauth/token

Exchange credentials for an access token

Client Credentials Grant

For server-to-server integrations where no user interaction is needed.

curl -X POST https://api.mailodds.com/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=validate:read send:write"

Authorization Code Grant (with PKCE)

For apps acting on behalf of a user. Exchange the authorization code received after user consent.

curl -X POST https://api.mailodds.com/oauth/token \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE_FROM_REDIRECT" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "code_verifier=YOUR_PKCE_VERIFIER"

Response (200)

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
  "scope": "validate:read send:write"
}

Available Scopes

ScopeDescription
validate:readRead validation results and job status
validate:writeCreate validation jobs and validate emails
send:readRead sending domains and email events
send:writeSend emails and manage sending domains
campaign:readRead campaigns and templates
campaign:writeCreate, update, and send campaigns
store:readRead store connections and products
store:writeManage store connections and trigger syncs
fullFull access to all API resources (confidential clients only)

Rate Limiting

The token endpoint is rate limited to 20 requests per minute per client_id. Responses include Cache-Control: no-store and Pragma: no-cache per RFC 6749.

POST /oauth/revoke

Revoke an access or refresh token (RFC 7009)

Request

curl -X POST https://api.mailodds.com/oauth/revoke \
  -d "token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." \
  -d "token_type_hint=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response (200)

Always returns 200 regardless of whether the token was found, per RFC 7009. This prevents token scanning attacks.

{}
POST /oauth/introspect

Introspect a token to check its validity and metadata (RFC 7662)

Request

curl -X POST https://api.mailodds.com/oauth/introspect \
  -d "token=eyJhbGciOiJSUzI1NiIs..." \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response (200) - Active Token

{
  "active": true,
  "scope": "validate:read send:write",
  "client_id": "your-client-id",
  "username": "42",
  "token_type": "Bearer",
  "exp": 1742486400,
  "iat": 1742482800,
  "account_id": 5
}

Response (200) - Inactive/Expired Token

{
  "active": false
}

Discovery Endpoints

These public endpoints allow clients to discover server capabilities and validate tokens without hardcoding URLs.

MethodEndpointDescription
GET/.well-known/oauth-authorization-serverServer metadata (RFC 8414): supported grant types, scopes, endpoints, and PKCE methods
GET/.well-known/jwks.jsonJSON Web Key Set containing the public keys used to sign access tokens

Server Metadata Response

{
  "issuer": "https://api.mailodds.com",
  "authorization_endpoint": "https://api.mailodds.com/oauth/authorize",
  "token_endpoint": "https://api.mailodds.com/oauth/token",
  "revocation_endpoint": "https://api.mailodds.com/oauth/revoke",
  "introspection_endpoint": "https://api.mailodds.com/oauth/introspect",
  "jwks_uri": "https://api.mailodds.com/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": [
    "authorization_code",
    "client_credentials",
    "refresh_token"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "client_secret_basic",
    "none"
  ],
  "code_challenge_methods_supported": ["S256"]
}

Store Connections (Growth+)

Connect e-commerce stores (WooCommerce, Shopify, PrestaShop) to synchronize product catalogs for use in campaign personalization and product recommendation emails.

POST /v1/stores

Connect a new e-commerce store

Request

curl -X POST https://api.mailodds.com/v1/stores \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "woocommerce",
    "store_name": "My Shop",
    "store_url": "https://myshop.com",
    "credentials": {
      "consumer_key": "ck_...",
      "consumer_secret": "cs_..."
    }
  }'

Response (201)

{
  "store": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "account_id": 5,
    "platform": "woocommerce",
    "store_name": "My Shop",
    "store_url": "https://myshop.com",
    "status": "active",
    "auth_method": "api_key",
    "product_count": 0,
    "last_synced_at": null,
    "last_error": null,
    "sync_interval_seconds": 3600,
    "settings": {},
    "created_at": "2026-03-13T12:00:00Z",
    "updated_at": "2026-03-13T12:00:00Z"
  }
}

Supported Platforms

PlatformAuth MethodRequired Credentials
woocommerceAPI keyconsumer_key, consumer_secret
shopifyOAuth / API keyaccess_token
prestashopAPI keyapi_key
GET /v1/stores

List all connected stores

Request

curl https://api.mailodds.com/v1/stores \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "stores": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "platform": "woocommerce",
      "store_name": "My Shop",
      "store_url": "https://myshop.com",
      "status": "active",
      "product_count": 142,
      "last_synced_at": "2026-03-13T11:00:00Z",
      ...
    }
  ]
}
POST /v1/stores/{store_id}/sync

Trigger a manual product catalog sync

Request

curl -X POST https://api.mailodds.com/v1/stores/{store_id}/sync \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "scheduled": true,
  "store_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

The store must be in active status to trigger a sync. Products are synchronized automatically based on the configured sync_interval_seconds, but you can trigger a manual sync at any time.

Other Store Endpoints

MethodEndpointDescription
GET/v1/stores/{store_id}Get store details, sync status, and product count
DELETE/v1/stores/{store_id}Disconnect a store and remove synced products

Store Products

Query and retrieve product data synchronized from your connected stores. Use products in campaign templates for personalized product recommendations.

GET /v1/store-products

Query products across all connected stores

Query Parameters

ParameterTypeDescription
store_idstringFilter by store
categorystringFilter by product category
stock_statusstringFilter by stock status (e.g. instock, outofstock)
on_salebooleanFilter to products currently on sale
searchstringSearch by title or SKU
pageintegerPage number (default: 1)
per_pageintegerResults per page (default: 20, max: 100)

Request

curl "https://api.mailodds.com/v1/store-products?on_sale=true&per_page=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "products": [
    {
      "id": "prod_abc123",
      "store_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "external_id": "456",
      "sku": "WIDGET-001",
      "title": "Premium Widget",
      "description": "A high-quality widget for all your needs.",
      "price_current": 29.99,
      "price_original": 39.99,
      "currency": "USD",
      "sale_start": "2026-03-01T00:00:00Z",
      "sale_end": "2026-03-31T23:59:59Z",
      "stock_status": "instock",
      "stock_quantity": 150,
      "image_url": "https://myshop.com/images/widget.jpg",
      "categories": ["Electronics", "Gadgets"],
      "tags": ["sale", "featured"],
      "product_url": "https://myshop.com/products/premium-widget",
      "is_active": true,
      "created_at": "2026-03-10T08:00:00Z",
      "updated_at": "2026-03-13T11:00:00Z"
    }
  ],
  "total": 42,
  "page": 1,
  "per_page": 10
}
GET /v1/store-products/{product_id}

Get a single product by ID

Request

curl https://api.mailodds.com/v1/store-products/{product_id} \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

Returns the full product object with all fields including additional images, tags, and sale dates.

Deliverability

Monitor your sender reputation, understand domain warmup behavior, and analyze bounces to maintain high inbox placement rates.

Sender Health Scoring

A composite 0 to 100 score reflecting your sending reputation, authentication setup, bounce rate, and complaint rate.

GET /v1/sender-health
curl https://api.mailodds.com/v1/sender-health \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "score": 85,
  "factors": [
    { "name": "bounce_rate", "value": 0.8, "weight": 0.3 },
    { "name": "complaint_rate", "value": 0.02, "weight": 0.25 },
    { "name": "authentication", "value": 1.0, "weight": 0.2 },
    { "name": "list_quality", "value": 0.92, "weight": 0.15 },
    { "name": "engagement", "value": 0.68, "weight": 0.1 }
  ],
  "recommendation": "Your sender health is excellent. Continue monitoring bounce rates after large sends."
}

Score Interpretation

RangeRatingAction
80 - 100ExcellentNo action required. You are in great standing with inbox providers.
60 - 79GoodReview factors and address any that score below average.
40 - 59Needs attentionInvestigate bounce and complaint sources. Validate your lists before sending.
0 - 39CriticalPause sending immediately. Run a bounce analysis and clean your lists.

Domain Warmup

MailOdds automatically warms new sending domains over a 42-day schedule. No API calls are required to manage warmup.

When you register a new sending domain, MailOdds enforces a graduated sending schedule that increases volume day by day. This builds sender reputation with inbox providers and prevents your emails from being flagged as spam.

Warmup Schedule Overview

PhaseDaysDaily VolumeNotes
Initial1 - 750 - 200Low volume to establish presence
Ramp8 - 21200 - 5,000Gradual increase, monitored for bounce spikes
Scale22 - 425,000 - 50,000+Approaching full capacity
Full43+Plan limitWarmup complete, sending at full rate

ISP-Tiered Rate Limiting

Gmail, Yahoo, and Microsoft each have separate warmup tracks with independent daily limits. This prevents a large send to one provider from consuming your quota for another. The warmup progress for each ISP tier is visible in the sender health response under the factors array.

Tip: Warmup is fully automatic. You do not need to call any API endpoint to start, pause, or configure it. If you exceed the warmup limit for a given day, excess emails are queued and delivered the following day.

Bounce Analysis

Analyze bounce patterns across your sending history to identify problem domains, stale addresses, and reputation risks.

Start a Bounce Analysis

POST /v1/bounce-analyses
curl -X POST https://api.mailodds.com/v1/bounce-analyses \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"days": 30}'

The days field specifies how far back to analyze (1 to 90, default 30). The analysis runs asynchronously and returns immediately with status processing.

Get Analysis Results

GET /v1/bounce-analyses/{id}
curl https://api.mailodds.com/v1/bounce-analyses/ba_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"

Analysis Response (200)

{
  "id": "ba_abc123",
  "status": "completed",
  "period_days": 30,
  "total_sent": 48500,
  "hard_bounces": 312,
  "soft_bounces": 89,
  "hard_bounce_rate": 0.64,
  "soft_bounce_rate": 0.18,
  "top_domains": [
    { "domain": "old-isp.example.com", "bounces": 48, "type": "hard" },
    { "domain": "corp-mail.example.net", "bounces": 31, "type": "hard" }
  ],
  "recommendation": "Remove 312 hard-bounced addresses from your lists to protect sender reputation.",
  "created_at": "2026-03-12T14:00:00Z",
  "completed_at": "2026-03-12T14:02:15Z"
}

Get Bounce Records

GET /v1/bounce-analyses/{id}/records
curl "https://api.mailodds.com/v1/bounce-analyses/ba_abc123/records?page=1&per_page=50" \
  -H "Authorization: Bearer YOUR_API_KEY"

Records Response (200)

{
  "items": [
    {
      "email": "user@old-isp.example.com",
      "bounce_type": "hard",
      "diagnostic": "550 5.1.1 User unknown",
      "bounced_at": "2026-03-10T08:12:00Z"
    },
    {
      "email": "admin@corp-mail.example.net",
      "bounce_type": "hard",
      "diagnostic": "550 5.1.1 Mailbox not found",
      "bounced_at": "2026-03-09T15:44:00Z"
    }
  ],
  "page": 1,
  "per_page": 50,
  "total": 401
}

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

{
  "success": false,
  "error": "invalid_api_key",
  "message": "Invalid or missing API key"
}
402

Payment Required

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

{
  "success": false,
  "error": "insufficient_credits",
  "message": "Not enough credits to perform this validation",
  "credits_available": 0,
  "credits_needed": 1,
  "upgrade_url": "https://mailodds.com/dashboard/billing"
}
403

Forbidden

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

429

Too Many Requests

Rate limit exceeded. Check the Retry-After header for when to retry.

{
  "success": false,
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Try again in 60 seconds.",
  "retry_after": 60
}
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. Sending API rate limits are documented in the Email Sending section. Need more? See Enterprise plans.

Ready to Get Started?

Sign up for a free account and start validating, sending, and managing subscribers in minutes

Create Free Account