Skip to content
mailchecker
  1. Home
  2. 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.

shell
# 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

RouteWhat it does
GET /v1/validateOne address, synchronous
POST /v1/validate/batchUp to 100 addresses, synchronous
POST /v1/filesUploads a list (CSV/XLSX)
GET /v1/files/{id}Progress and totals of a list
GET /v1/files/{id}/downloadA list’s results as CSV
GET /v1/creditsCredit balance
GET /v1/usageUsage by day and status
GET /v1/openapi.jsonOpenAPI 3.1 spec
GET

/v1/validate?email=

Checks one address and returns a Verdict. Parameter: email (required).

request
curl "https://api.mailchecker.dev/v1/validate?email=emma.jones@gmail.com" \
  -H "Authorization: Bearer mc_live_…"
response · 200
{
  "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"]
}
POST

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

request
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"]}'
response · 200
{
  "results": [ /* one Verdict per address */ ],
  "cost": 2
}
POST

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

request
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"
GET

/v1/files/{id}

The list’s state: uploaded, queued, running, done, failed or cancelled, with totals by status. Poll it until done.

response · 200
{
  "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
}
GET

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

GET

/v1/credits

The team’s balance: {"balance": 1840}.

GET

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

FieldTypeDescription
addressstringThe normalized address (no spaces, lowercase domain).
statusstringvalid, invalid, catch-all, unknown, spamtrap, abuse or do_not_mail.
sub_statusstringDetail of the status (list below); "" when there is none.
accountstringThe part before the @.
domainstringThe part after the @.
did_you_meanstring | nullSuggested fix for a typo in the domain (gmial.com → gmail.com).
free_emailbooleanA free mailbox provider (Gmail, Outlook, Yahoo…).
role_basedbooleanA role address: info@, sales@, support@…
disposablebooleanA disposable/temporary email service.
mx_foundbooleanThe domain has a mail server.
mx_recordstring | nullThe highest-priority MX.
smtp_providerstring | nullgoogle, microsoft, yahoo, proofpoint, mimecast, zoho, other…
domain_age_daysinteger | nullDomain age in days (RDAP).
processed_atstringWhen the check ran, ISO 8601 (UTC).
domain_scoreinteger | nullDomain 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_reasonsstring[]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.

CodeMeaning
disposable_domaindisposable domain
blocklist_spamhaus_dbllisted on Spamhaus DBL
blocklist_surbllisted on SURBL
blocklist_uribllisted on URIBL
blocklist_spamhaus_zenMX listed on Spamhaus ZEN
blocklist_barracudaMX listed on Barracuda
blocklist_unknownblocklist did not answer (no penalty)
parked_domainparked or for-sale domain
null_mxsays it accepts no mail (null MX)
no_mxno MX record
mx_ip_literalMX is an IP, not a hostname
mx_private_ipMX points to a private or reserved IP
mx_no_addressMX host has no IP address
domain_age_lt_30ddomain registered less than 30 days ago
domain_age_lt_1ydomain registered less than a year ago
domain_age_unknowndomain age unknown (no penalty)
no_spfno SPF
spf_invalidinvalid SPF (more than one record)
spf_plus_allSPF lets anyone send (+all)
no_dmarcno DMARC
dmarc_noneDMARC only monitors (p=none)
free_emailfree email provider (no penalty)
role_basedrole-based address (no penalty)

Statuses and sub-statuses

The same vocabulary as ZeroBounce.

StatusMeaningPossible 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-statusMeaning
mailbox_not_foundThe domain’s mail server said this mailbox doesn’t exist.
no_dns_entriesThe domain has no valid MX or A records, so there is nowhere to deliver to.
failed_syntax_checkThe address isn’t a well-formed email address (RFC 5322).
possible_typoThe domain looks like a typo of a well-known provider (see the “did you mean” suggestion).
unroutable_ip_addressThe domain’s MX points to a private or reserved IP that can’t be reached from the internet.
does_not_accept_mailThe domain says it doesn’t receive email (null MX) or rejects every message.
mailbox_quota_exceededThe mailbox is over its quota and is rejecting messages.
leading_period_removedThe address started with a period, which isn’t allowed; we checked it without it.
role_basedA role address (info@, sales@, support@): usually read by several people and more likely to report spam.
disposableA temporary, throwaway email domain: the address will stop existing soon.
toxicAn address or domain known for abuse, spam or bots.
global_suppressionOn the global suppression list (asked not to be emailed, repeated complaints).
role_based_catch_allA role address on a domain that accepts any address: high risk, low chance of being read.
alias_addressAn alias that forwards to another mailbox; send to the main address instead.
mx_forwardThe domain only forwards mail to another service; the final mailbox couldn’t be confirmed.
greylistedThe server deferred its answer (greylisting). Trying again later usually works.
antispam_systemAn anti-spam filter blocked the check before it could tell whether the mailbox exists.
mail_server_temporary_errorThe mail server answered with a temporary error (4xx).
mail_server_did_not_respondThe mail server didn’t answer the connection.
timeout_exceededThe server took too long to answer and the check was stopped.
failed_smtp_connectionWe couldn’t open an SMTP connection to the domain’s mail server.
forcible_disconnectThe server dropped the connection in the middle of the check.
exception_occurredAn unexpected error stopped the check. No credit was charged.
possible_trapThe 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.

response · 422
{
  "error": "validation",
  "message": "…",
  "fields": { "email": ["…"] }
}
HTTPerrorWhen
401unauthorizedMissing, invalid or revoked key.
402no_creditNot enough credits. For a list, processing stops with state=failed and error=no_credit.
422validationA parameter is missing or invalid; see fields.
429rate_limitedThe 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: unknown and duplicates are free.
before and after
# 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.