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
actionfield for decisioning) - Processing large lists via the real-time endpoint (use bulk jobs)
- Sending to unvalidated recipient lists (enable
validate_firstor bulk-validate before sending) - Importing subscribers without double opt-in (consent protects deliverability and compliance)
Choose Your Integration Path
Real-Time Validation
Validate at form submission. Synchronous, sub-300ms typical latency.
POST /v1/validate Bulk Validation
Upload CSV or JSON lists. Up to 100k emails. Async processing.
Email Sending
Send transactional and campaign emails. Every recipient validated before delivery.
POST /v1/deliver Subscriber Lists
Double opt-in subscriber management with suppression enforcement.
POST /v1/subscribe/:list_id Webhooks
Get notified when bulk jobs complete. Non-blocking async workflows.
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 |
|---|---|---|
| Validation | ||
| POST | /v1/validate | Validate a single email address |
| POST | /v1/validate/batch | Validate up to 50 emails in one request |
| 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 |
| Suppression | ||
| 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/suppression/stats | Get suppression list statistics |
| Policies | ||
| GET | /v1/policies | List validation policies |
| POST | /v1/policies | Create validation policy |
| POST | /v1/policies/test | Test policy evaluation |
| Sending Domains | ||
| POST | /v1/sending-domains | Register a sending domain |
| GET | /v1/sending-domains | List sending domains |
| GET | /v1/sending-domains/:id | Get domain details and DNS records |
| DELETE | /v1/sending-domains/:id | Remove a sending domain |
| POST | /v1/sending-domains/:id/verify | Trigger DNS verification |
| GET | /v1/sending-domains/:id/identity-score | Get domain identity score |
| Email Sending | ||
| POST | /v1/deliver | Send a single email |
| POST | /v1/deliver/batch | Send to multiple recipients |
| GET | /v1/sending-stats | Get sending statistics |
| Subscriber Lists | ||
| POST | /v1/lists | Create a subscriber list |
| GET | /v1/lists | List all subscriber lists |
| GET | /v1/lists/:id | Get list details and subscriber count |
| DELETE | /v1/lists/:id | Delete a subscriber list |
| POST | /v1/subscribe/:list_id | Subscribe an email to a list |
| GET | /v1/confirm/:token | Confirm double opt-in subscription |
| Campaigns | ||
| POST | /v1/campaigns | Create a campaign |
| POST | /v1/campaigns/:id/schedule | Schedule a campaign |
| POST | /v1/campaigns/:id/send | Send campaign immediately |
| POST | /v1/campaigns/:id/cancel | Cancel a scheduled or sending campaign |
| POST | /v1/campaigns/:id/variants | Create A/B test variant |
| GET | /v1/campaigns/:id/ab-results | Get A/B test results |
| GET | /v1/campaigns/:id/funnel | Get campaign funnel metrics |
| GET | /v1/campaigns/:id/delivery-confidence | Get delivery confidence score |
| OAuth 2.0 | ||
| POST | /oauth/token | Exchange credentials for an access token |
| POST | /oauth/revoke | Revoke an access or refresh token |
| POST | /oauth/introspect | Introspect a token (check validity) |
| GET | /.well-known/oauth-authorization-server | OAuth server metadata (RFC 8414) |
| GET | /.well-known/jwks.json | JSON Web Key Set (public keys) |
| Store Connections | ||
| GET | /v1/stores | List all store connections |
| POST | /v1/stores | Connect a new store |
| GET | /v1/stores/:id | Get store details and sync status |
| DELETE | /v1/stores/:id | Disconnect a store |
| POST | /v1/stores/:id/sync | Trigger a manual product sync |
| GET | /v1/store-products | Query synced products |
| GET | /v1/store-products/:id | Get a single product |
| Deliverability | ||
| GET | /v1/sender-health | Get sender health score |
| POST | /v1/bounce-analyses | Start bounce analysis |
| GET | /v1/bounce-analyses/:id | Get bounce analysis results |
| GET | /v1/bounce-analyses/:id/records | Get individual bounce records |
| Telemetry | ||
| GET | /v1/telemetry/summary | Get validation metrics for dashboards |
OpenAPI Specification
OpenAPI 3.2.0 compliant specification
Use our machine-readable OpenAPI specification to generate client libraries, import into API tools like Postman or Insomnia, or integrate with your existing development workflow.
Authentication
All API requests require authentication using a Bearer token. Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEYGetting Your API Key
Sign up for a free account to get your API key from the dashboard. Each account includes a generous free tier.
Test Mode
For integration testing, use API keys with the mo_test_ prefix
combined with special test domains that return predictable results without consuming credits.
Test Domains
| Email Domain | Result | Description |
|---|---|---|
*@deliverable.mailodds.com | valid / accept | Always deliverable |
*@invalid.mailodds.com | invalid / reject | Mailbox not found |
*@risky.mailodds.com | catch_all / accept_with_caution | Catch-all detected |
*@disposable.mailodds.com | do_not_mail / reject | Disposable email |
*@role.mailodds.com | do_not_mail / reject | 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 do not consume credits when used with mo_test_ keys.
Validating real emails (non-test domains) with test keys still consumes credits normally.
Validate Email Endpoint
/v1/validateRequest Body
{
"email": "user@example.com"
}Response (Valid Email)
{
"schema_version": "1.0",
"email": "user@example.com",
"status": "valid",
"action": "accept",
"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:
status -- What we found (factual) valid, invalid, catch_all, unknown, do_not_mail
action -- What you should do (recommended) accept, accept_with_caution, reject, retry_later
sub_status -- Why (detail, informational) disposable, role_account, greylisted, mx_missing, 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
| 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 |
mx_timeout | DNS lookup timed out for domain | retry_later |
domain_not_found | Domain does not exist (NXDOMAIN) | reject |
suppression_match | Matched your suppression list | reject |
restricted_military | Military domain restricted by compliance policy | reject |
restricted_sanctioned | Domain restricted by sanctions/export controls | reject |
Bulk Validation (Pro+)
Validate large lists of emails asynchronously. Upload a CSV file or submit a JSON array of up to 100,000 emails. Results are processed in the background and you'll be notified via webhook when complete.
/jobsCreate a bulk validation job from a JSON array of emails
Request Body
{
"emails": [
"user1@example.com",
"user2@example.com",
"user3@example.com"
],
"dedup": true, // optional - remove duplicates before processing (saves credits)
"callback_url": "https://your-server.com/webhook", // optional
"metadata": {"campaign": "newsletter"} // optional
}Save Credits with Deduplication
Set dedup: true to automatically remove duplicate emails before processing. This is case-insensitive and ensures you only pay for unique emails.
Idempotency
Include an Idempotency-Key header with a unique value (max 64 chars). If a request with the same key was made within 24 hours, the existing job is returned instead of creating a duplicate.
Response
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "pending",
"total_count": 3,
"created_at": "2026-01-30T12:00:00Z"
}
}/v1/jobs/uploadCreate a bulk validation job by uploading a CSV, Excel, or TXT file
Request (multipart/form-data)
curl -X POST https://api.mailodds.com/v1/jobs/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@emails.csv" \
-F "dedup=true" \
-F 'metadata={"campaign": "newsletter"}'Supported File Formats
CSV: First column should contain emails (header optional)
Excel (.xlsx, .xls): First column of first sheet should contain emails
ODS (OpenDocument): First column of first sheet should contain emails
TXT: One email per line
Max 100,000 emails per job.
Save Credits with Deduplication
Add -F "dedup=true" to remove duplicate emails before processing. This is case-insensitive and ensures you only pay for unique emails.
/v1/jobs/upload/presignedGet a presigned S3 URL for uploading large files (>10MB) directly to cloud storage
When to Use S3 Upload
For files larger than 10MB, use S3 presigned upload to avoid timeouts. This is a 3-step process: get presigned URL, upload to S3, then create the job.
Step 1: Get Presigned Upload Credentials
curl -X POST https://api.mailodds.com/v1/jobs/upload/presigned \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "large_list.csv", "content_type": "text/csv"}'Response
{
"schema_version": "1.0",
"upload": {
"url": "https://s3.amazonaws.com/mailodds-uploads",
"fields": {
"key": "uploads/abc123/large_list.csv",
"AWSAccessKeyId": "...",
"policy": "...",
"signature": "..."
},
"s3_key": "uploads/abc123/large_list.csv",
"expires_in": 3600
}
}Step 2: Upload File to S3
curl -X POST "${URL}" \
-F "key=${KEY}" \
-F "AWSAccessKeyId=${ACCESS_KEY_ID}" \
-F "policy=${POLICY}" \
-F "signature=${SIGNATURE}" \
-F "file=@large_list.csv"Step 3: Create Job from S3 File
Use the s3_key from Step 1 to create the validation job:
curl -X POST https://api.mailodds.com/v1/jobs/upload/s3 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"s3_key": "uploads/abc123/large_list.csv", "dedup": true}'Note: S3 upload is optional and only available when configured. Files are automatically deleted from S3 after job creation.
/v1/jobs/{job_id}Check the status and progress of a bulk validation job
Response (Processing)
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "processing",
"total_count": 1000,
"processed_count": 450,
"progress_percent": 45,
"created_at": "2026-01-30T12:00:00Z"
}
}Response (Completed)
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "completed",
"total_count": 1000,
"processed_count": 1000,
"progress_percent": 100,
"summary": {
"valid": 850,
"invalid": 80,
"catch_all": 40,
"unknown": 20,
"do_not_mail": 10
},
"created_at": "2026-01-30T12:00:00Z",
"completed_at": "2026-01-30T12:05:00Z"
}
}/v1/jobs/{job_id}/resultsDownload validation results as CSV or NDJSON
Query Parameters
format string - Output format: csv (default) or ndjson
filter string - Filter results: valid_only (deliverable emails) or invalid_only (undeliverable emails). Omit for all results.
dedup boolean - Set to true to remove duplicate emails from results (case-insensitive).
sort string - Sort order: sequence (upload order, default) or recent (newest first).
signed_url string - Return a pre-signed URL instead of direct download: csv or ndjson. URL expires after 1 hour.
Example: Download valid emails only (deduplicated)
curl "https://api.mailodds.com/v1/jobs/job_abc123xyz/results?format=csv&filter=valid_only&dedup=true" \
-H "Authorization: Bearer YOUR_API_KEY" \
-o valid_emails.csv/v1/jobs/{job_id}/streamSubscribe to real-time job progress updates via Server-Sent Events (SSE)
Authentication via Query Parameter
For EventSource compatibility, pass your API key as a token query parameter instead of the Authorization header.
Events
status Initial job state on connectprogress Processing update (every 100 emails)done Job completed, cancelled, or failederror Error occurredExample (JavaScript)
const eventSource = new EventSource(
'https://api.mailodds.com/v1/jobs/job_xyz789/stream?token=mo_live_xxx'
);
eventSource.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
console.log(`Progress: ${data.percent}%`);
});
eventSource.addEventListener('done', (e) => {
console.log('Job completed!');
eventSource.close();
});Webhooks
Get notified when jobs complete (Pro+ plans)
Configure a webhook URL in your dashboard settings or provide a callback_url when creating a job. We'll POST events when your job completes or fails.
Webhook Payload (job.completed)
{
"event": "job.completed",
"job": {
"id": "job_abc123xyz",
"status": "completed",
"total_count": 1000,
"summary": {
"valid": 850,
"invalid": 80,
"catch_all": 40,
"unknown": 20,
"do_not_mail": 10
}
},
"timestamp": "2026-01-30T12:05:00Z"
}Webhook Headers
X-MailOdds-Event Event type (e.g., job.completed, message.delivered)X-MailOdds-Signature HMAC-SHA256 signature for verificationX-Request-Id Unique request identifier for tracingUser-Agent MailOdds-Webhook/1.0Operational 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 3 times with exponential backoff: 10s, 60s, 300s. |
| 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 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
Job created, waiting to start processing
Currently validating emails
All emails validated, results ready for download
Job encountered an error during processing
Suppression List API
Manage your suppression list to automatically block specific emails, domains, or patterns during validation. Emails matching your suppression list return status: "do_not_mail" with sub_status: "suppression_match".
/v1/suppressionList suppression entries with optional filtering and pagination
Query Parameters
type Filter by type: email, domain, or patternsearch Search by value or reasonpage, per_page Pagination (default: page=1, per_page=50)/v1/suppressionAdd one or more entries to your suppression list
Request Body
{
"entries": [
{
"type": "email",
"value": "spam@example.com",
"reason": "Unsubscribed"
},
{
"type": "domain",
"value": "competitor.com",
"reason": "Competitor domain"
},
{
"type": "pattern",
"value": ".*@tempmail\\..*",
"reason": "Temporary email pattern"
}
]
}Entry Types
email: Block a specific email address. domain: Block all emails from a domain. pattern: Block emails matching a regex pattern.
/v1/suppressionRemove entries from your suppression list
Request Body
{
"ids": [123, 456, 789]
}/v1/suppression/checkCheck if an email matches your suppression list without counting as a validation
Request Body
{
"email": "test@competitor.com"
}Response (Match Found)
{
"suppressed": true,
"match": {
"type": "domain",
"value": "competitor.com",
"reason": "Competitor domain"
}
}Suppression in Validation Response
When a validated email matches your suppression list
{
"email": "blocked@competitor.com",
"status": "do_not_mail",
"action": "reject",
"sub_status": "suppression_match",
"free_email": false,
"suppression": {
"match_type": "domain",
"match_value": "competitor.com",
"reason": "Competitor domain"
}
}Validation Policies API
Create rules to customize how validation results are interpreted. Override default actions based on status, domain, check results, or reason codes.
Plan Limits
Free plans: 1 policy, 3 rules max. Pro+ plans: Unlimited policies and rules.
Rule Types
| Type | Description | Example Condition |
|---|---|---|
status_override | Match by validation status | {"status": "catch_all"} |
domain_filter | Match by domain allowlist/blocklist | {"domain_mode": "blocklist", "domains": [...]} |
check_requirement | Require specific check to pass | {"check": "smtp", "required": true} |
sub_status_override | Match by reason code | {"sub_status": "role_account"} |
/v1/policiesList all validation policies for your account
Example
curl https://api.mailodds.com/v1/policies \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"policies": [
{
"id": 1,
"name": "Strict Policy",
"description": "Reject catch-all and role accounts",
"is_default": true,
"is_enabled": true,
"rule_count": 3
}
],
"limits": {
"max_policies": -1,
"max_rules_per_policy": -1,
"plan": "pro"
}
}/v1/policies/presetsGet available preset templates for quick policy creation
Available Presets
- strict - Reject catch-all, role accounts, and unknown status
- permissive - Accept catch-all and role accounts with caution
- smtp_required - Require SMTP verification to pass
/v1/policiesCreate a new validation policy with rules
Example
curl -X POST https://api.mailodds.com/v1/policies \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Strict Policy",
"description": "Reject catch-all emails",
"is_default": true,
"rules": [
{
"type": "status_override",
"condition": {"status": "catch_all"},
"action": {"action": "reject"}
}
]
}'Or Create from Preset
curl -X POST https://api.mailodds.com/v1/policies/from-preset \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"preset_id": "strict", "name": "My Strict Policy"}'/v1/policies/testTest how a policy would evaluate a validation result
Example
curl -X POST https://api.mailodds.com/v1/policies/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"policy_id": 1,
"test_result": {
"email": "test@catch-all-domain.com",
"status": "catch_all",
"action": "accept_with_caution"
}
}'Response
{
"original": {
"status": "catch_all",
"action": "accept_with_caution"
},
"modified": {
"status": "catch_all",
"action": "reject"
},
"matched_rule": {
"id": 1,
"type": "status_override",
"condition": {"status": "catch_all"}
},
"rules_evaluated": 3
}Policy Applied in Validation Response
When a policy modifies a validation result
{
"email": "info@example.com",
"status": "valid",
"action": "reject",
"policy_applied": {
"policy_id": 1,
"policy_name": "Strict Policy",
"rule_id": 3,
"rule_type": "sub_status_override"
}
}Job Management
Cancel or delete bulk validation jobs.
/v1/jobs/{job_id}/cancelCancel a pending or processing job. Partial results are preserved.
Example
curl -X POST https://api.mailodds.com/v1/jobs/job_abc123xyz/cancel \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"schema_version": "1.0",
"job": {
"id": "job_abc123xyz",
"status": "cancelled",
"total_count": 1000,
"processed_count": 450,
"cancelled_pending": 550, // emails not validated due to cancellation
"progress_percent": 45
}
}Partial Results
You can still download results for emails that were validated before cancellation. The cancelled_pending field shows how many emails were skipped.
/v1/jobs/{job_id}Permanently delete a completed or cancelled job and its results
Example
curl -X DELETE https://api.mailodds.com/v1/jobs/job_abc123xyz \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"schema_version": "1.0",
"deleted": true,
"job_id": "job_abc123xyz"
}Irreversible Action
Deletion is permanent. All validation results for this job will be removed and cannot be recovered. Running jobs cannot be deleted - cancel them first.
Telemetry
Monitor your validation metrics to build dashboards and track performance.
/v1/telemetry/summaryGet validation metrics for your account
Query Parameters
window string - Time window: 1h (last hour), 24h (default), or 30d (last 30 days)
group_by string - Group results by: api_key (breakdown per API key). Omit for aggregate totals.
Example
curl "https://api.mailodds.com/v1/telemetry/summary?window=24h" \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"schemaVersion": "1.0",
"window": "24h",
"generatedAt": "2026-01-31T12:00:00Z",
"timezone": "UTC",
"totals": {
"validations": 12500,
"creditsUsed": 12500
},
"rates": {
"deliverable": 0.872,
"reject": 0.084,
"unknown": 0.044,
"suppressed": 0.003
},
"topReasons": [
{"reason": "smtp_failed", "count": 450},
{"reason": "disposable", "count": 230}
],
"topDomains": [
{"domain": "gmail.com", "volume": 4500, "deliverable": 0.94}
]
}ETag Caching
This endpoint supports ETag-based caching. Include If-None-Match header with the previous ETag to receive 304 Not Modified when data hasn't changed.
Code Examples
cURL
curl -X POST https://api.mailodds.com/v1/validate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"email": "user@example.com"}'JavaScript (Fetch API)
const validateEmail = async (email) => {
const response = await fetch('https://api.mailodds.com/v1/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY'
},
body: JSON.stringify({ email })
});
if (!response.ok) {
if (response.status === 429) {
// Rate limited: wait and retry
await new Promise(r => setTimeout(r, 2000));
return validateEmail(email);
}
if (response.status >= 500) {
// Server error: fail open
return { action: 'accept', fallback: true };
}
throw new Error(`API error: ${response.status}`);
}
return response.json();
};
// Usage
const result = await validateEmail('user@example.com');
if (result.action === 'accept') {
console.log('Email is valid');
}Python (requests)
import requests
from time import sleep
def validate_email(email, max_retries=3):
url = 'https://api.mailodds.com/v1/validate'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY'
}
for attempt in range(max_retries):
try:
resp = requests.post(url, json={'email': email}, headers=headers, timeout=10)
if resp.status_code == 200:
return resp.json()
if resp.status_code in (429, 500, 502, 503):
sleep(2 ** attempt)
continue
return {'error': resp.status_code}
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
continue
# Fail open: allow signup, validate later
return {'action': 'accept', 'fallback': True}
# Usage
result = validate_email('user@example.com')
if result.get('action') == 'accept':
print('Email is valid')PHP
<?php
function validateEmail($email) {
$ch = curl_init('https://api.mailodds.com/v1/validate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'email' => $email
]));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Usage
$result = validateEmail('user@example.com');
print_r($result);
?>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.
/v1/sending-domainsRegister 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.
| Record | You Configure | MailOdds Manages |
|---|---|---|
| NS | mo.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 |
| DMARC | You manage DMARC on your root domain | - |
Domain Status Values
| Status | Description |
|---|---|
pending_dns | Waiting for NS records to be configured |
dns_partial | Some records verified, others still pending |
active | All records verified, ready to send |
suspended | Domain suspended (DNS records removed or policy violation) |
/v1/sending-domains/{id}/verifyTrigger 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.
/v1/sending-domains/{id}/identity-scoreGet 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
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/sending-domains | List 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.
/v1/deliverSend 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
| Field | Type | Required | Description |
|---|---|---|---|
to | object[] | Yes | Recipients. Each object: email (required), name (optional) |
from | string | Yes | Sender address (must match sending domain) |
subject | string | Yes | Email subject line |
domain_id | string | Yes | Sending domain UUID |
html | string | * | HTML body (html or text required) |
text | string | * | Plain text body (html or text required) |
reply_to | string | No | Reply-to address |
headers | object | No | Custom email headers |
tags | string[] | No | Tags for filtering in stats/webhooks |
campaign_type | string | No | Auto-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_data | object | array | string | No | Explicit JSON-LD structured data (max 10KB). Takes priority over campaign_type auto-generation. |
schema_data | object | No | Key-value pairs for campaign_type JSON-LD resolution (e.g., order_number, tracking_url) |
auto_detect_schema | boolean | No | Auto-detect JSON-LD type from subject line (default: false) |
ai_summary | string | No | Hidden text summary for AI email assistants (max 500 characters) |
options.validate_first | boolean | No | Pre-validate recipients (default: true) |
options.tracking_domain | string | No | Override 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
}
}/v1/deliver/batchSend 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.
/v1/sending-statsGet 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
| Endpoint | Limit |
|---|---|
POST /v1/deliver | 10 requests/second per account |
POST /v1/deliver/batch | 5 requests/second per account |
POST /v1/subscribe/:id | 10/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.
| Event | Description |
|---|---|
message.queued | Email accepted and queued for delivery |
message.delivered | Email delivered to recipient |
message.bounced | Email bounced (hard or soft) |
message.deferred | Delivery temporarily deferred |
message.failed | Delivery permanently failed |
message.opened | Recipient opened the email |
message.clicked | Recipient 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.
/v1/listsCreate 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).
/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
- Email is pre-validated (invalid addresses are rejected immediately)
- Subscriber is created with status
pending - Confirmation email is sent with a unique token link
- When the subscriber clicks the link (
GET /v1/confirm/{token}), status changes toconfirmed - 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
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/lists | List 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}/subscribers | List 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
/v1/campaignscurl -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
/v1/campaigns/{id}/schedulecurl -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
/v1/campaigns/{id}/sendcurl -X POST https://api.mailodds.com/v1/campaigns/camp_7f3a1b2c4d5e/send \
-H "Authorization: Bearer YOUR_API_KEY"Cancel a Campaign
/v1/campaigns/{id}/cancelcurl -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
| Status | Description |
|---|---|
draft | Campaign created, not yet sent or scheduled |
scheduled | Queued for delivery at the specified send_at time |
sending | Delivery is in progress |
sent | All emails delivered successfully |
cancelled | Campaign 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
/v1/campaigns/{id}/variantscurl -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
/v1/campaigns/{id}/ab-resultscurl 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
/v1/campaigns/{id}/funnelcurl 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
/v1/campaigns/{id}/delivery-confidencecurl 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.
/oauth/tokenExchange 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
| Scope | Description |
|---|---|
validate:read | Read validation results and job status |
validate:write | Create validation jobs and validate emails |
send:read | Read sending domains and email events |
send:write | Send emails and manage sending domains |
campaign:read | Read campaigns and templates |
campaign:write | Create, update, and send campaigns |
store:read | Read store connections and products |
store:write | Manage store connections and trigger syncs |
full | Full 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.
/oauth/revokeRevoke 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.
{}/oauth/introspectIntrospect 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.
| Method | Endpoint | Description |
|---|---|---|
| GET | /.well-known/oauth-authorization-server | Server metadata (RFC 8414): supported grant types, scopes, endpoints, and PKCE methods |
| GET | /.well-known/jwks.json | JSON 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.
/v1/storesConnect 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
| Platform | Auth Method | Required Credentials |
|---|---|---|
| woocommerce | API key | consumer_key, consumer_secret |
| shopify | OAuth / API key | access_token |
| prestashop | API key | api_key |
/v1/storesList 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",
...
}
]
}/v1/stores/{store_id}/syncTrigger 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
| Method | Endpoint | Description |
|---|---|---|
| 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.
/v1/store-productsQuery products across all connected stores
Query Parameters
| Parameter | Type | Description |
|---|---|---|
store_id | string | Filter by store |
category | string | Filter by product category |
stock_status | string | Filter by stock status (e.g. instock, outofstock) |
on_sale | boolean | Filter to products currently on sale |
search | string | Search by title or SKU |
page | integer | Page number (default: 1) |
per_page | integer | Results 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
}/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.
/v1/sender-healthcurl 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
| Range | Rating | Action |
|---|---|---|
80 - 100 | Excellent | No action required. You are in great standing with inbox providers. |
60 - 79 | Good | Review factors and address any that score below average. |
40 - 59 | Needs attention | Investigate bounce and complaint sources. Validate your lists before sending. |
0 - 39 | Critical | Pause 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
| Phase | Days | Daily Volume | Notes |
|---|---|---|---|
| Initial | 1 - 7 | 50 - 200 | Low volume to establish presence |
| Ramp | 8 - 21 | 200 - 5,000 | Gradual increase, monitored for bounce spikes |
| Scale | 22 - 42 | 5,000 - 50,000+ | Approaching full capacity |
| Full | 43+ | Plan limit | Warmup 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
/v1/bounce-analysescurl -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
/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
/v1/bounce-analyses/{id}/recordscurl "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:
OK
Request was successful
Bad Request
Invalid request format or missing required fields
Unauthorized
Missing or invalid API key
{
"success": false,
"error": "invalid_api_key",
"message": "Invalid or missing API key"
}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"
}Forbidden
Feature not available on your plan (e.g., batch validation requires Pro+)
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
}Internal Server Error
Something went wrong on our end
Recommended Retry Pattern
async function validateWithRetry(email, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const resp = await fetch('https://api.mailodds.com/v1/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(10000)
});
if (resp.ok) return await resp.json();
if (resp.status === 429 || resp.status >= 500) {
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
continue; // retry
}
// 400, 401, 402, 403: do not retry
return { error: resp.status, body: await resp.json() };
} catch (e) {
if (attempt < maxRetries - 1) continue;
// All retries failed: fail open
return { action: 'accept', fallback: true };
}
}
// All retries exhausted: fail open
return { action: 'accept', fallback: true };
}Never block signups because MailOdds is unreachable
If the API times out or returns 5xx after retries, allow the signup and re-validate the email later. Your conversion matters more than a single validation check. This is called the "fail open" pattern.
402 Response Body
When you receive a 402, the response body contains everything you need to handle it programmatically:
{
"error": "insufficient_credits",
"message": "Not enough credits to validate 500 emails...",
"credits_available": 12,
"credits_needed": 500,
"current_plan": "starter",
"upgrade_url": "https://mailodds.com/dashboard/billing",
"recommended_action": "purchase_credits"
}Scaling & Best Practices
Do
- Cache validation results. An email validated today does not need re-validation tomorrow. Cache for 30-90 days.
- Use bulk jobs for lists of 50+ emails. More cost-efficient and does not consume rate limit.
- Use webhooks (Pro+) to get notified when bulk jobs finish instead of polling.
- Set
dedup: trueon bulk jobs to avoid paying for duplicate emails. - Use idempotency keys on bulk job creation to prevent duplicate jobs on retries.
Do Not
- Validate the same email on every page load. Validate once at signup, then cache the result.
- Use
/v1/validatefor CSV imports. Use /v1/jobs/upload instead. - Block your UI on a third-party API call. Run validation async when possible.
- Ignore
accept_with_cautionresults. These are legitimate emails that deserve extra scrutiny, not rejection.
Production Checklist
Before you go live
Review these items before deploying to production
API key stored in environment variable
Never hardcode API keys in source code
Error handling implemented
Retry on 429/5xx, fail open on timeout
Decision logic branches on action
Not just status. See Understanding Results
Rate limits understood for your plan
Check rate limits and handle 429 responses
Bulk jobs used for list processing
Not the real-time /v1/validate endpoint
Test mode verified with mo_test_ key
Use test domains before switching to live key
Caching strategy in place
Avoid re-validating known emails. Cache results for 30-90 days
Rate Limits
Rate limits vary by plan to ensure fair usage and service quality:
Free
50
validations/month
10/sec
Starter
5,000
validations/month
50/sec
Pro
25,000
validations/month
100/sec
Business
100,000
validations/month
200/sec
Rate limit headers are included in all API responses to help you track your usage. 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