Overview
All requests go to the base URL below over HTTPS. Requests and responses use JSON, and responses are UTF-8 encoded.
Authentication
Authenticate every request with your API key. The recommended method is the Authorization header:
Authorization: Bearer ev_your_api_keyThe 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
Validates a single address. Use GET with a query parameter, or POST a JSON body.
Parameters
| Name | In | Description |
|---|---|---|
email | query / body | Required. The address to validate. |
fresh | query | Optional. 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"
curl -X POST "https://validateemail.io/v1/validate" \
-H "Authorization: Bearer $VALIDATEEMAIL_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jane.doe@gmail.com"}'
const res = await fetch("https://validateemail.io/v1/validate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VALIDATEEMAIL_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "jane.doe@gmail.com" }),
});
const result = await res.json();
Batch validation
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"
}| Field | Type | Description |
|---|---|---|
email | string | The address exactly as you sent it. |
normalized | string | null | Trimmed, lower-cased address with the domain in punycode. null if the syntax is invalid. |
user, domain | string | null | The local part and domain. |
status | string | valid, invalid, risky or unknown. See statuses. |
reason | string | Machine-readable reason for the status. |
score | number | Confidence from 0 (certainly bad) to 1 (very likely good). |
did_you_mean | string | null | Suggested correction when the domain looks like a typo of a popular provider. |
checks.syntax | boolean | The address is well-formed. |
checks.domain_exists | boolean | null | The domain exists in DNS. null if DNS could not be reached. |
checks.mx_found | boolean | null | The domain has a mail server (MX record, or an A/AAAA fallback). |
checks.disposable | boolean | null | The domain is a known disposable or temporary inbox provider. |
checks.role_based | boolean | null | The local part is a shared/role inbox such as info or support. |
checks.free_provider | boolean | null | The domain is a consumer mailbox provider such as Gmail or Outlook. |
checks.smtp | null | Reserved for mailbox-level verification. Not performed yet, so it is always null. |
mx_records | array | The domain's mail servers, sorted by priority. |
cached | boolean | true if this result was served from the cache. |
checked_at | string | ISO 8601 time the checks ran. |
Statuses & reasons
| Status | Reason | Meaning |
|---|---|---|
valid | mail_server_found | Well-formed, the domain accepts mail, and no risk signals were found. |
risky | disposable | A temporary/throwaway inbox provider. |
risky | possible_typo | The domain closely resembles a popular provider. Check did_you_mean. |
risky | no_mx_records | No MX records. Mail would fall back to the domain's A/AAAA record, which is unusual for real mailboxes. |
risky | role_based | A shared inbox (info@, support@, …). |
invalid | domain_not_found | The domain does not exist. |
invalid | domain_rejects_mail | The domain publishes a "null MX" record, so it explicitly accepts no mail. |
invalid | no_mail_server | The domain has no mail server at all. |
invalid | syntax reasons | e.g. missing_at_or_part, invalid_domain, invalid_local_part_characters, local_part_too_long, invalid_tld. |
unknown | dns_error | DNS 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" } }| HTTP | Code | When |
|---|---|---|
| 400 | bad_request | Missing or malformed email / emails. |
| 400 | batch_too_large | More than 100 addresses in a batch. |
| 401 | unauthorized | No API key, or the key is invalid or revoked. |
| 405 | method_not_allowed | Wrong HTTP method for the endpoint. |
| 500 | internal_error | Something went wrong on our side. Safe to retry. |
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, reviewrisky. 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
unknownas 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.