Email validation API reference

Everything you need to validate email addresses with validateemail.io: one base URL, bearer-token auth, and JSON responses.

Overview

All requests go to the base URL below over HTTPS. Requests and responses use JSON, and responses are UTF-8 encoded.

https://validateemail.io
API keys are issued to waitlist members as paid plans open. Register your interest to get yours, or try the live demo in the meantime.

Authentication

Authenticate every request with your API key. The recommended method is the Authorization header:

Authorization: Bearer ev_your_api_key

The API also accepts an X-API-Key: ev_your_api_key header, or an api_key query parameter for quick tests. Keep your key on your server. Anyone who has it can use your quota.

Validate an address

GET/v1/validate?email={address}
POST/v1/validate

Validates a single address. Use GET with a query parameter, or POST a JSON body.

Parameters

NameInDescription
emailquery / bodyRequired. The address to validate.
freshqueryOptional. true bypasses the cache and re-runs every check. The new result is cached again.

Example

curl "https://validateemail.io/v1/validate?email=jane.doe@gmail.com" \
  -H "Authorization: Bearer $VALIDATEEMAIL_KEY"

Batch validation

POST/v1/validate/batch

Validates up to 100 addresses in one request. Results come back in the same order you sent them. Each distinct domain is looked up once per batch, so batches are much faster than the same number of single calls.

curl -X POST "https://validateemail.io/v1/validate/batch" \
  -H "Authorization: Bearer $VALIDATEEMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["jane@gmail.com", "info@acme.io", "bob@gmial.com"]}'

The response is an object with a results array. Each entry has the same shape as a single-address response:

{ "results": [ { "email": "jane@gmail.com", "status": "valid", … }, … ] }

Response format

{
  "email": "bob@gmial.com",
  "normalized": "bob@gmial.com",
  "user": "bob",
  "domain": "gmial.com",
  "status": "risky",
  "reason": "possible_typo",
  "score": 0.5,
  "did_you_mean": "bob@gmail.com",
  "checks": {
    "syntax": true,
    "domain_exists": true,
    "mx_found": true,
    "disposable": false,
    "role_based": false,
    "free_provider": false,
    "smtp": null
  },
  "mx_records": [{ "exchange": "mx.gmial.com", "priority": 10 }],
  "cached": false,
  "checked_at": "2026-10-21T14:37:17.479Z"
}
FieldTypeDescription
emailstringThe address exactly as you sent it.
normalizedstring | nullTrimmed, lower-cased address with the domain in punycode. null if the syntax is invalid.
user, domainstring | nullThe local part and domain.
statusstringvalid, invalid, risky or unknown. See statuses.
reasonstringMachine-readable reason for the status.
scorenumberConfidence from 0 (certainly bad) to 1 (very likely good).
did_you_meanstring | nullSuggested correction when the domain looks like a typo of a popular provider.
checks.syntaxbooleanThe address is well-formed.
checks.domain_existsboolean | nullThe domain exists in DNS. null if DNS could not be reached.
checks.mx_foundboolean | nullThe domain has a mail server (MX record, or an A/AAAA fallback).
checks.disposableboolean | nullThe domain is a known disposable or temporary inbox provider.
checks.role_basedboolean | nullThe local part is a shared/role inbox such as info or support.
checks.free_providerboolean | nullThe domain is a consumer mailbox provider such as Gmail or Outlook.
checks.smtpnullReserved for mailbox-level verification. Not performed yet, so it is always null.
mx_recordsarrayThe domain's mail servers, sorted by priority.
cachedbooleantrue if this result was served from the cache.
checked_atstringISO 8601 time the checks ran.

Statuses & reasons

StatusReasonMeaning
validmail_server_foundWell-formed, the domain accepts mail, and no risk signals were found.
riskydisposableA temporary/throwaway inbox provider.
riskypossible_typoThe domain closely resembles a popular provider. Check did_you_mean.
riskyno_mx_recordsNo MX records. Mail would fall back to the domain's A/AAAA record, which is unusual for real mailboxes.
riskyrole_basedA shared inbox (info@, support@, …).
invaliddomain_not_foundThe domain does not exist.
invaliddomain_rejects_mailThe domain publishes a "null MX" record, so it explicitly accepts no mail.
invalidno_mail_serverThe domain has no mail server at all.
invalidsyntax reasonse.g. missing_at_or_part, invalid_domain, invalid_local_part_characters, local_part_too_long, invalid_tld.
unknowndns_errorDNS could not be resolved right now. This is temporary, so retry later.

When several risk signals apply, the most significant one is reported as the reason. Every signal is still visible in checks.

Errors

Errors use standard HTTP status codes and a consistent JSON body:

{ "error": { "code": "unauthorized", "message": "Invalid or revoked API key" } }
HTTPCodeWhen
400bad_requestMissing or malformed email / emails.
400batch_too_largeMore than 100 addresses in a batch.
401unauthorizedNo API key, or the key is invalid or revoked.
405method_not_allowedWrong HTTP method for the endpoint.
500internal_errorSomething went wrong on our side. Safe to retry.
An invalid email address is not an error. It returns 200 OK with "status": "invalid".

Caching

To keep responses fast, results are cached: per address for 7 days, and per domain (DNS and list checks) for 24 hours. A cached response has "cached": true and keeps the original checked_at. Temporary DNS failures are never cached. Add fresh=true to force a re-check.

Best practices

  • Call from your backend. Your key should never ship in browser or mobile code.
  • Block invalid, review risky. Decide per use case. A newsletter might accept role accounts, while a free trial might reject disposable inboxes.
  • Show did_you_mean. Offering the fix inline recovers sign-ups that would otherwise bounce.
  • Treat unknown as a soft pass. Accept the address and re-validate later rather than blocking a real user.
  • Use batch for lists. It's one round-trip, and shared domains are only looked up once.