- Home
- API
API v1
Email validation API
REST and JSON. Validate one address in real time, batches of up to 100, or whole files. Base URL: https://api.mailchecker.dev
Overview
Every route lives under https://api.mailchecker.dev/v1, answers JSON in UTF-8 (except result downloads, which are CSV) and needs an API key. Create and revoke keys in the dashboard, under API keys; the full key is shown only once.
The OpenAPI 3.1 spec is at https://api.mailchecker.dev/v1/openapi.json.
Authentication
Send the key (mc_live_…) in the Authorization: Bearer header or, for ZeroBounce compatibility, in the api_key query parameter. Prefer the header: URLs tend to end up in logs.
# Header (recommended)
curl "https://api.mailchecker.dev/v1/credits" \
-H "Authorization: Bearer mc_live_…"
# Or in the query string, as with ZeroBounce
curl "https://api.mailchecker.dev/v1/credits?api_key=mc_live_…"
Endpoints
| Route | What it does |
|---|---|
GET /v1/validate | One address, synchronous |
POST /v1/validate/batch | Up to 100 addresses, synchronous |
POST /v1/files | Uploads a list (CSV/XLSX) |
GET /v1/files/{id} | Progress and totals of a list |
GET /v1/files/{id}/download | A list’s results as CSV |
GET /v1/credits | Credit balance |
GET /v1/usage | Usage by day and status |
GET /v1/openapi.json | OpenAPI 3.1 spec |
/v1/validate?email=
Checks one address and returns a Verdict. Parameter: email (required).
curl "https://api.mailchecker.dev/v1/validate?email=emma.jones@gmail.com" \
-H "Authorization: Bearer mc_live_…"
{
"address": "emma.jones@gmail.com",
"status": "valid",
"sub_status": "",
"account": "emma.jones",
"domain": "gmail.com",
"did_you_mean": null,
"free_email": true,
"role_based": false,
"disposable": false,
"mx_found": true,
"mx_record": "gmail-smtp-in.l.google.com",
"smtp_provider": "google",
"domain_age_days": 11318,
"processed_at": "2026-11-07T12:34:56Z",
"domain_score": 100,
"domain_reasons": ["dmarc_none", "free_email"]
}
/v1/validate/batch
Body {"emails": [...]} with up to 100 addresses. Returns one Verdict per address and the batch’s total cost; repeats within the batch aren’t charged.
curl -X POST "https://api.mailchecker.dev/v1/validate/batch" \
-H "Authorization: Bearer mc_live_…" \
-H "Content-Type: application/json" \
-d '{"emails": ["emma@company.com", "john@gmial.com"]}'
{
"results": [ /* one Verdict per address */ ],
"cost": 2
}
/v1/files
Multipart with file (CSV or XLSX, up to 1 million rows), column (optional: the email column) and name. Answers 201 with the list in the queued state.
curl -X POST "https://api.mailchecker.dev/v1/files" \
-H "Authorization: Bearer mc_live_…" \
-F "file=@customers.csv" -F "column=2" -F "name=November customers"
/v1/files/{id}
The list’s state: uploaded, queued, running, done, failed or cancelled, with totals by status. Poll it until done.
{
"id": "0193f1c2-…",
"name": "November customers",
"original_filename": "customers.csv",
"state": "running",
"total": 12480, "unique_count": 12011, "duplicates": 469,
"processed": 8300,
"counts": { "valid": 6904, "invalid": 811, "catch-all": 402, "unknown": 183 },
"cost": 8117,
"error": null,
"created_at": "2026-11-07T12:00:00Z", "started_at": "2026-11-07T12:00:02Z", "finished_at": null
}
/v1/files/{id}/download?filter=
The results as CSV. filter=all (default) returns everything, valid only the valid addresses, no_invalid everything except the invalid ones.
/v1/credits
The team’s balance: {"balance": 1840}.
/v1/usage?from=&to=
Daily usage by status, with YYYY-MM-DD dates: {"days": [{"day", "status", "count", "cost"}]}.
The result (Verdict)
Every checked address becomes an object with these fields:
| Field | Type | Description |
|---|---|---|
address | string | The normalized address (no spaces, lowercase domain). |
status | string | valid, invalid, catch-all, unknown, spamtrap, abuse or do_not_mail. |
sub_status | string | Detail of the status (list below); "" when there is none. |
account | string | The part before the @. |
domain | string | The part after the @. |
did_you_mean | string | null | Suggested fix for a typo in the domain (gmial.com → gmail.com). |
free_email | boolean | A free mailbox provider (Gmail, Outlook, Yahoo…). |
role_based | boolean | A role address: info@, sales@, support@… |
disposable | boolean | A disposable/temporary email service. |
mx_found | boolean | The domain has a mail server. |
mx_record | string | null | The highest-priority MX. |
smtp_provider | string | null | google, microsoft, yahoo, proofpoint, mimecast, zoho, other… |
domain_age_days | integer | null | Domain age in days (RDAP). |
processed_at | string | When the check ran, ISO 8601 (UTC). |
domain_score | integer | null | Domain reputation, 0–100 (higher = safer), from DNS and registry (RDAP) lookups only, never by contacting the mail server. Never changes status. null when not computed. |
domain_reasons | string[] | Reasons behind the score: stable codes in a fixed order (list below). |
Domain reputation reasons
Each reason takes points off 100; the ones marked "no penalty" are informational. Free email domains (Gmail, Outlook…) only lose points for blocklists.
| Code | Meaning |
|---|---|
disposable_domain | disposable domain |
blocklist_spamhaus_dbl | listed on Spamhaus DBL |
blocklist_surbl | listed on SURBL |
blocklist_uribl | listed on URIBL |
blocklist_spamhaus_zen | MX listed on Spamhaus ZEN |
blocklist_barracuda | MX listed on Barracuda |
blocklist_unknown | blocklist did not answer (no penalty) |
parked_domain | parked or for-sale domain |
null_mx | says it accepts no mail (null MX) |
no_mx | no MX record |
mx_ip_literal | MX is an IP, not a hostname |
mx_private_ip | MX points to a private or reserved IP |
mx_no_address | MX host has no IP address |
domain_age_lt_30d | domain registered less than 30 days ago |
domain_age_lt_1y | domain registered less than a year ago |
domain_age_unknown | domain age unknown (no penalty) |
no_spf | no SPF |
spf_invalid | invalid SPF (more than one record) |
spf_plus_all | SPF lets anyone send (+all) |
no_dmarc | no DMARC |
dmarc_none | DMARC only monitors (p=none) |
free_email | free email provider (no penalty) |
role_based | role-based address (no penalty) |
Statuses and sub-statuses
The same vocabulary as ZeroBounce.
| Status | Meaning | Possible sub-statuses |
|---|---|---|
| valid | The mailbox exists and the server accepted the recipient. Safe to send. | — |
| invalid | The address can’t receive mail: no such mailbox, no mail server for the domain, a typo or bad syntax. Remove it: this is what causes bounces. | mailbox_not_found, no_dns_entries, failed_syntax_check, possible_typo, unroutable_ip_address, does_not_accept_mail, mailbox_quota_exceeded, leading_period_removed |
| catch-all | The domain accepts every address, so the mailbox can’t be confirmed. Send with care, ideally in smaller batches. | — |
| unknown | The server didn’t give a conclusive answer (greylisting, anti-spam, timeout). You’re not charged: try again later. | greylisted, antispam_system, mail_server_temporary_error, mail_server_did_not_respond, timeout_exceeded, failed_smtp_connection, forcible_disconnect, exception_occurred |
| do_not_mail | The address exists, but sending is risky: disposable, role-based (info@, sales@), toxic or on a suppression list. | role_based, disposable, toxic, global_suppression, role_based_catch_all, alias_address, mx_forward |
| spamtrap | Likely a spam trap: a single send can get your domain blocklisted. | possible_trap |
| abuse | An address known for marking mail as spam and reporting senders. | — |
What each sub-status means
| Sub-status | Meaning |
|---|---|
mailbox_not_found | The domain’s mail server said this mailbox doesn’t exist. |
no_dns_entries | The domain has no valid MX or A records, so there is nowhere to deliver to. |
failed_syntax_check | The address isn’t a well-formed email address (RFC 5322). |
possible_typo | The domain looks like a typo of a well-known provider (see the “did you mean” suggestion). |
unroutable_ip_address | The domain’s MX points to a private or reserved IP that can’t be reached from the internet. |
does_not_accept_mail | The domain says it doesn’t receive email (null MX) or rejects every message. |
mailbox_quota_exceeded | The mailbox is over its quota and is rejecting messages. |
leading_period_removed | The address started with a period, which isn’t allowed; we checked it without it. |
role_based | A role address (info@, sales@, support@): usually read by several people and more likely to report spam. |
disposable | A temporary, throwaway email domain: the address will stop existing soon. |
toxic | An address or domain known for abuse, spam or bots. |
global_suppression | On the global suppression list (asked not to be emailed, repeated complaints). |
role_based_catch_all | A role address on a domain that accepts any address: high risk, low chance of being read. |
alias_address | An alias that forwards to another mailbox; send to the main address instead. |
mx_forward | The domain only forwards mail to another service; the final mailbox couldn’t be confirmed. |
greylisted | The server deferred its answer (greylisting). Trying again later usually works. |
antispam_system | An anti-spam filter blocked the check before it could tell whether the mailbox exists. |
mail_server_temporary_error | The mail server answered with a temporary error (4xx). |
mail_server_did_not_respond | The mail server didn’t answer the connection. |
timeout_exceeded | The server took too long to answer and the check was stopped. |
failed_smtp_connection | We couldn’t open an SMTP connection to the domain’s mail server. |
forcible_disconnect | The server dropped the connection in the middle of the check. |
exception_occurred | An unexpected error stopped the check. No credit was charged. |
possible_trap | The address shows signs of a spam trap (set by mailbox providers to catch bought lists). |
Errors
Every error has the same shape: a stable code in error, a human-readable message in message and, for validation errors, the offending fields in fields.
{
"error": "validation",
"message": "…",
"fields": { "email": ["…"] }
}
| HTTP | error | When |
|---|---|---|
| 401 | unauthorized | Missing, invalid or revoked key. |
| 402 | no_credit | Not enough credits. For a list, processing stops with state=failed and error=no_credit. |
| 422 | validation | A parameter is missing or invalid; see fields. |
| 429 | rate_limited | The key’s request limit was exceeded; wait and try again. |
Limits
- Requests: a per-key token bucket; the default is 10 requests per second. Need more? Get in touch.
- Synchronous batch: up to 100 addresses per
POST /v1/validate/batch. - Files: CSV or XLSX with up to 1 million rows.
- Retention: results are available for 30 days and then deleted (see our Privacy Policy).
Billing
Credits are debited as each result is stored: 1 credit per address, catch-all included. unknown costs 0, and so do repeated addresses within the same list or batch. Check your balance with GET /v1/credits and see pricing.
Migrating from ZeroBounce
We designed the API so switching is quick:
- The same statuses and sub-statuses, with the same names.
- The same names for the main result fields (
address,status,sub_status,account,domain,did_you_mean,free_email,mx_found,mx_record,smtp_provider,domain_age_days,processed_at). - The key is accepted in
?api_key=. - The same billing rules:
unknownand duplicates are free.
# ZeroBounce
GET https://api.zerobounce.net/v2/validate?api_key=YOUR_KEY&email=emma@company.com
# mailchecker: swap the base URL and the key
GET https://api.mailchecker.dev/v1/validate?api_key=mc_live_…&email=emma@company.com
What changes: the base URL and path (/v1/...), the key, and the name, gender and location fields, which this version doesn’t have. We add role_based and disposable as booleans. Check the field types in your parser before switching in production.
See also the full comparison.