REST Countries

Docs Email Intelligence API

The Email Intelligence API is a read-only API for reasoning about an email address. Every endpoint returns JSON and authenticates via a single bearer token. No SDK is required.

Overview

The Email Intelligence API is a single GET process endpoint. Give it an email address in the query string and it answers with one object in data.objects describing the address: its parts, what's known about its domain (including the domain's live mail exchangers), and a set of boolean attributes — whether it's a free mailbox, a throwaway, or a forwarding alias.

  • Coverage. 13,000+ free mailbox domains, 8,000+ throwaway domains, 190+ role-based local parts and 250+ country top-level domains, plus 34 mail operators named from MX records. Each field's reference section states what its list reaches and where it stops.
  • Read-only. All endpoints are GET. There is no POST/PUT/DELETE surface.
  • JSON-only responses. Content-Type: application/json on every response. Successful payloads sit under a top-level data key with the record in data.objects; failures come back under errors as an array of objects carrying a message.
  • One shape, every time. Every response carries the identical structure. A value the data can't name for a given address comes back null. There are no optional keys to feature-detect.
  • One required parameter. The address itself. Everything else on the request is optional.

Try it now

Two ways to try the Email Intelligence API right now. Neither requires an account.

1. Fire a live request from this page. Click the button below. The response slides open in the Explorer panel.

// Demo key: no account required.
const response = await fetch(
  'https://api.restcountries.com/email-intelligence/v1/[email protected]',
  { headers: { 'Authorization': 'Bearer rc_live_demo' } }
);
const data = await response.json();

Run this request →

2. Run it from your terminal. Click the copy button below, then paste anywhere curl lives.

curl "https://api.restcountries.com/email-intelligence/v1/[email protected]&pretty=1" \
  -H "Authorization: Bearer rc_live_demo"

The demo key returns a real, correctly-shaped sample record plus a data._demo notice block; it never touches your account or quota.

Quick start

Sign up to get an API key, then paste this into your terminal (swap out the API key placeholder):

curl "https://api.restcountries.com/email-intelligence/v1/[email protected]&pretty=1" \
  -H "Authorization: Bearer YOUR_API_KEY"

You'll get a single-object data.objects array back, with the domain's mail exchangers under domain.mx. Drop the email parameter to see the key come back null. From there, browse the Process section for the full surface, or the Response objects reference to see every field.

Base URL & versioning

Email Intelligence API endpoints live under:

https://api.restcountries.com/email-intelligence/v1

The v1 path segment is the API version. Backwards-compatible additions (new fields, new enrichments) ship inside it without a bump. See Versioning.

Append ?pretty to any request to pretty-print the JSON response, which is handy while exploring from a browser or terminal.

Process

GET /email-intelligence/v1/process

Runs one email address through the pipeline. The address is read from the email query parameter, or from q as an alias, and is required: a request naming neither is rejected with a 400. The address is echoed back verbatim; beyond requiring one, no syntax validation runs against it yet. It's split into parts, and the domain half of that split drives a live MX lookup, so domain.mx reflects the domain's published records rather than a stored copy of them. See Freshness for how briefly results are reused.

Path examples

Free mailbox provider
GET /email-intelligence/v1/[email protected] Run →
Plus-addressed alias
GET /email-intelligence/v1/process?email=someone%[email protected] Run →
Address as the q alias
GET /email-intelligence/v1/[email protected] Run →
Corporate domain on Google Workspace — a paid seat, not a free mailbox
GET /email-intelligence/v1/[email protected] Run →
Microsoft-hosted mailbox
GET /email-intelligence/v1/[email protected] Run →
Privacy-focused provider
GET /email-intelligence/v1/[email protected] Run →
Role-based address
GET /email-intelligence/v1/[email protected] Run →
Automated sender
GET /email-intelligence/v1/[email protected] Run →
Japanese ccTLD — 🇯🇵 Japan
GET /email-intelligence/v1/[email protected] Run →
German ccTLD — 🇩🇪 Germany
GET /email-intelligence/v1/[email protected] Run →
French ccTLD — 🇫🇷 France
GET /email-intelligence/v1/[email protected] Run →
Mexican ccTLD — 🇲🇽 Mexico
GET /email-intelligence/v1/[email protected] Run →
Supranational suffix — no country, category government
GET /email-intelligence/v1/[email protected] Run →
US academic suffix — no country, category education
GET /email-intelligence/v1/[email protected] Run →
Misspelling — transposed letters
GET /email-intelligence/v1/[email protected] Run →
Misspelling — truncated suffix
GET /email-intelligence/v1/[email protected] Run →
Misspelling — doubled letter
GET /email-intelligence/v1/[email protected] Run →
Misspelling — dropped letter
GET /email-intelligence/v1/[email protected] Run →
Misspelling — dropped letter, different provider
GET /email-intelligence/v1/[email protected] Run →
Misspelling — n for m in the suffix
GET /email-intelligence/v1/[email protected] Run →
Misspelling — suffix cut short
GET /email-intelligence/v1/[email protected] Run →
Misspelling — transposed suffix
GET /email-intelligence/v1/[email protected] Run →
Typo-squatted — resolves, but still corrected from the curated list
GET /email-intelligence/v1/[email protected] Run →
Malformed — 200, is_valid_syntax false, no lookups
GET /email-intelligence/v1/process?email=not-an-email Run →
Published SPF, with a DMARC reject policy
GET /email-intelligence/v1/[email protected] Run →
Disposable domain
GET /email-intelligence/v1/[email protected] Run →
Disposable by MX only — the domain itself is on no list
GET /email-intelligence/v1/[email protected] Run →
Filtering gateway in front of the mailbox host
GET /email-intelligence/v1/[email protected] Run →
Forwarding / alias service
GET /email-intelligence/v1/[email protected] Run →
Domain that accepts no mail at all
GET /email-intelligence/v1/[email protected] Run →

Query parameters

Every parameter below is optional unless it carries a required badge.

email string required
Email address to run through the pipeline. Also accepted under the alias q; email wins when both are present. A request naming neither is rejected with a 400 — see Errors.
response_fields string
Allowlist of properties to include in the returned object. When set, the response carries only those properties and nothing else. Comma-separated dot-paths; ordering doesn't matter. Omit the parameter to return every field.

It also makes the request faster, in the same way response_fields_omit does: a lookup whose every field you left out is skipped rather than performed and discarded. response_fields=email,parts makes no outbound calls at all.
response_fields_omit string
Blocklist of properties to drop from the returned object. Comma-separated dot-paths; ordering doesn't matter. Composes with response_fields: when both are present, the response is first projected to response_fields, then response_fields_omit prunes from that subset (so the omit list wins on conflict).

It also makes the request faster. Omitting a branch that needs a lookup skips the lookup entirely — response_fields_omit=domain.authentication drops two TXT queries, omitting just domain.authentication.spf drops one, and gravatar or domain.registration each drop an outbound HTTP call to a third party.

The MX lookup is only skipped when every field computed from it is omitted — domain.mx, domain.provider, domain.category, attributes.is_disposable, attributes.is_forwarder and did_you_mean all read those records. Dropping domain.mx on its own still runs it, because the others would otherwise answer as though the domain had no mail servers at all.
pretty boolean
Pretty-print the JSON response. The bare flag (?pretty) and truthy values (true, 1) enable; anything else (including false and 0) leaves the response compact.

Example

Request
curl "https://api.restcountries.com/email-intelligence/v1/[email protected]&pretty=1" \
  -H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
  'https://api.restcountries.com/email-intelligence/v1/[email protected]',
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const data = await response.json();
fetch(
  'https://api.restcountries.com/email-intelligence/v1/[email protected]',
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
)
  .then(function (response) { return response.json(); })
  .then(function (data) { console.log(data); });
import requests
response = requests.get(
  'https://api.restcountries.com/email-intelligence/v1/[email protected]',
  headers={'Authorization': 'Bearer YOUR_API_KEY'}
)
data = response.json()
$ch = curl_init('https://api.restcountries.com/email-intelligence/v1/[email protected]');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer YOUR_API_KEY']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);
require 'net/http'
require 'json'
uri = URI('https://api.restcountries.com/email-intelligence/v1/[email protected]')
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer YOUR_API_KEY'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end
data = JSON.parse(response.body)
package main
import (
  "encoding/json"
  "io"
  "net/http"
)
req, _ := http.NewRequest("GET", "https://api.restcountries.com/email-intelligence/v1/[email protected]", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(body, &data)
use reqwest::blocking::Client;
use serde_json::Value;
let client = Client::new();
let response = client
  .get("https://api.restcountries.com/email-intelligence/v1/[email protected]")
  .header("Authorization", "Bearer YOUR_API_KEY")
  .send()?;
let data: Value = response.json()?;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("https://api.restcountries.com/email-intelligence/v1/[email protected]"))
  .header("Authorization", "Bearer YOUR_API_KEY")
  .GET()
  .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_API_KEY");
var data = await client.GetFromJsonAsync<JsonElement>(
  "https://api.restcountries.com/email-intelligence/v1/[email protected]");
import Foundation
var request = URLRequest(url: URL(string: "https://api.restcountries.com/email-intelligence/v1/[email protected]")!)
request.addValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data)
Response
{
  "data": {
    "objects": [
      {
        "email": "[email protected]",
        "did_you_mean": null,
        "parts": {
          "local": "someone+newsletter",
          "username": "someone",
          "tag": "newsletter",
          "domain": "gmail.com"
        },
        "tld": {
          "name": "com",
          "country": {
            "name": null,
            "emoji": null
          }
        },
        "domain": {
          "provider": {
            "name": "Google",
            "type": "mailbox",
            "url": "https://workspace.google.com"
          },
          "category": "free",
          "mx": [
            {
              "exchange": "gmail-smtp-in.l.google.com",
              "priority": 5
            },
            {
              "exchange": "alt1.gmail-smtp-in.l.google.com",
              "priority": 10
            },
            {
              "exchange": "alt2.gmail-smtp-in.l.google.com",
              "priority": 20
            },
            {
              "exchange": "alt3.gmail-smtp-in.l.google.com",
              "priority": 30
            },
            {
              "exchange": "alt4.gmail-smtp-in.l.google.com",
              "priority": 40
            }
          ],
          "registration": {
            "registered_on": "1995-08-13",
            "updated_on": "2026-07-11",
            "transferred_on": null,
            "expires_on": "2027-08-12",
            "age_days": 11316
          },
          "authentication": {
            "spf": {
              "present": true,
              "policy": "softfail",
              "record": "v=spf1 redirect=_spf.google.com"
            },
            "dmarc": {
              "present": true,
              "policy": "none",
              "subdomain_policy": "quarantine",
              "percentage": 100,
              "record": "v=DMARC1; p=none; sp=quarantine; rua=mailto:..."
            }
          }
        },
        "gravatar": {
          "exists": false,
          "avatar_url": null,
          "profile_url": null
        },
        "attributes": {
          "is_valid_syntax": true,
          "is_free": true,
          "is_disposable": false,
          "is_forwarder": false,
          "is_likely_role_based": false,
          "has_plus_addressing": true
        }
      }
    ]
  }
}

Response objects

Every request answers with a single record in data.objects, and every record carries the same complete shape: every key below is present on every response, with null marking a value the request didn't supply. There are no optional keys to feature-detect.

Top level

email string
The address the request resolved to: the email parameter, then the q alias. null when neither was supplied.
did_you_mean string
A corrected address when the domain looks like a typo of a well-known one, null otherwise — which is most of the time. See did_you_mean.
parts object
The address broken into its pieces. See parts below for each one.
tld object
The domain's top-level label and the country that holds it. See tld below.
domain object
What's known about the domain half of the address. provider names who operates its mail (see domain.provider), mx lists its mail exchangers (see domain.mx), and category buckets the kind of domain it is (see domain.category).
gravatar object
Whether the address has a Gravatar, and where it is. See gravatar below.
attributes object
Boolean flags about the address. See attributes below for each one.

parts

The address is split on its last @, since only the domain half is guaranteed free of them (a quoted local part may legally carry its own). The domain half is case-insensitive and comes back lowercased; the local half is not, and is preserved exactly as you sent it.

local string
Everything before the @, tag included.
username string
The local part with any plus-addressing tag removed — the mailbox the address actually delivers to. Identical to local when there's no tag.
tag string
The plus-addressing tag: everything after the local part's first +, which is the convention every provider supporting the feature follows (a second + is part of the tag, not a new delimiter). null when the address carries no + at all.
domain string
Everything after the last @, lowercased. This is the value the MX lookup runs against.

An address the split can't read — one with no @ in it — comes back with every piece at its default rather than an error. The split is structural, not a validity check: a well-formed-looking address and a deliverable one are different questions, and this answers neither.

tld

name string
The domain's top-level label: everything after its last dot, lowercased. This is the DNS top level, not the registrable suffix — example.co.uk gives uk, not co.uk. That is the right unit for this block, because which country administers a suffix is settled by the last label alone: co.uk, ac.uk and gov.uk are all the UK's.
country.name string
The country that holds the ccTLD, or null when the suffix belongs to no country — a generic TLD like com, or a supranational one like eu.

Names and territories come from the same dataset the Countries API serves, with nothing added by hand, so this field is exactly as accurate as that API and never disagrees with it.
country.emoji string
That country's flag emoji, or null alongside a null name. Names and flags come from the same dataset the Countries API serves, so they line up exactly across the two.

A few examples of what comes back:

Address tld.name tld.country
[email protected] jp 🇯🇵 Japan
[email protected] de 🇩🇪 Germany
[email protected] fr 🇫🇷 France
[email protected] uk 🇬🇧 United Kingdom
[email protected] mx 🇲🇽 Mexico
[email protected] eu null — not a country
[email protected] com null — generic, no country
This is provenance, not location. A ccTLD names who administers the suffix, and a great many are sold and used worldwide with no connection to the country at all — io, ai, tv, me and co are ordinary commercial domains for most of their registrants. Never read tld.country as a claim about where a domain's owner is.

domain.category

What kind of domain this is, as one mutually-exclusive value. Three of the buckets restate attributes the record already carries; the field earns its place on the rest, because a corporate domain, a university, a government body and a personal vanity domain otherwise all read false, false, false.

Precedence — first match wins. A disposable address is nearly always free as well, and a relay often is too; the order says which fact matters most to a caller, not which is truest.

# Value Meaning
1 disposable A throwaway domain.
2 forwarding Relays its mail somewhere else.
3 free A public mailbox provider.
4 education An academic suffix — edu, ac.uk, ac.jp.
5 government A public-body or military suffix — gov, mil, gc.ca.
6 custom Accepts mail and matched none of the above.
7 no_mail Publishes no way to receive mail at all.

Why custom and not "business". They are the same bucket, and "business" is the conventional word, but it is wrong at the edges: [email protected] is a person's vanity domain, and nothing in DNS separates that from a company. custom claims only what is actually known — the domain is controlled by whoever uses it, rather than being a public mailbox provider.

Note there is no role value, though other APIs put one in a field like this. A role account is a property of the address's local half — support@ and jane@ share a domain and differ on it — so it can't be a category of the domain. It lives on is_likely_role_based instead.

The academic and government suffix lists cover most of the world, not all of it. An unlisted institution falls through to custom.

domain.provider

The operator handling mail for the domain, read from the domain's mail exchangers and never from the domain name itself. name is the operator (Google, Microsoft, Proton), url its homepage, and type one of:

Value Meaning
mailbox Runs the mailbox itself. The plain reading of "provider".
gateway A filtering layer in front of the real mailbox host, which stays invisible from DNS.
forwarding Relays to a mailbox elsewhere.
disposable Hands out throwaway mailboxes.

All three keys are null when the operator isn't one we recognise, which includes every self-hosted mail server. That is a real answer rather than a gap: guessing from a partial hostname match would be worse than saying nothing.

Reading it from MX is what lets it stay correct across hosting arrangements: a company running its own domain on Google Workspace reports Google, exactly like a gmail.com address, because Google genuinely operates both. Note this is the opposite of how is_free behaves, and deliberately so — the same two addresses differ completely on that question.

It names who accepts the mail, which isn't always who stores it. Plenty of organisations put a filtering gateway in front of their real mailbox host — bbc.co.uk publishes MessageLabs, hsbc.com publishes Proofpoint — and the mailbox host behind it is invisible from DNS. In those cases provider.name is the gateway, because that is what the domain's records actually say, and provider.type is gateway so you can tell.

domain.mx

The mail exchangers published by the address's domain, resolved from DNS. Ordered the way a sending mail server would consider them: lowest priority first, ties broken alphabetically by exchange, so a repeat lookup of the same domain always returns the same order.

exchange string
Hostname of the mail exchanger, lowercased and without its trailing root dot.
priority number
The record's preference value. Lower is tried first.

The array comes back empty when the domain publishes no mail exchangers, when it declares that it accepts no mail at all (a null MX, as example.com does), when it doesn't resolve, and when the lookup itself fails. Those cases aren't distinguished: an empty mx means "no exchangers to show you", not "this address is undeliverable".

domain.authentication

What the domain publishes about who may send mail as it. Two TXT lookups, issued alongside the MX one, so they cost no extra wall-clock.

spf.present boolean
Whether the domain publishes an SPF record.
spf.policy string
What the record says to do with a sender it didn't authorise, named rather than symbolic: strict (-all, reject), softfail (~all, accept but mark — by far the most common), neutral (?all, no opinion), or allow_all (+all, which authorises the entire internet and is a misconfiguration).

A record delegating via redirect= is followed one hop, so gmail.com reports softfail rather than null.
dmarc.present boolean
Whether the domain publishes a DMARC record.
dmarc.policy string
none, quarantine or reject, straight from the record's p= tag.
dmarc.subdomain_policy string
The same, applied to subdomains, from the record's sp= tag. null when the record doesn't set one, in which case subdomains inherit dmarc.policy.
dmarc.percentage number
The share of mail the policy is applied to, from the record's pct= tag. Commonly used to roll a strict policy out gradually, so a reject at 10 is far weaker than it looks.

Both blocks carry the raw record alongside the parsed values, which is what you want when debugging someone's mail configuration.

A present: false means either the domain publishes nothing or the lookup couldn't be completed — the same ambiguity domain.mx carries, and for the same reason.

There is no DKIM here, and there can't be. A DKIM key lives at <selector>._domainkey.<domain>, where the selector is an arbitrary string the sender picks and publishes nowhere discoverable. Selectors can't be enumerated, so any "DKIM: present" answer is really a guess at a handful of common names. We'd rather omit it than fabricate it.

did_you_mean

A corrected address when the domain looks like a typo of a well-known one: [email protected] suggests [email protected], [email protected] the same. null whenever there's no confident answer, which is most of the time and deliberately so.

Matching is Damerau-Levenshtein — edit distance that counts a transposition as one edit rather than two. That matters more than it sounds: gmial.com is probably the most common Gmail typo there is, and plain edit distance scores it the same as unrelated domains. Keyboard adjacency breaks ties between equally-close candidates. Mistyped suffixes (.cm, .co, .con) are handled by their own map, since edit distance is bad at them.

Most well-known misspellings are typo-squatted. gnail.com, hotmial.com and iclod.com all publish MX records right now — they run real mail servers to catch misdirected mail. The MX guard would ordinarily silence the suggestion on exactly those, which is the opposite of helpful.

So a hand-verified list of known misspellings is checked first and overrides the guard. It is exact-match only, and every entry was confirmed by a person to be a typo rather than somebody's real domain — the trust is in the curation, not in any algorithm. A misspelling outside that list which resolves still gets silence, deliberately: "correcting" a domain that turns out to be a real business is the worse error. Where it matters, pair did_you_mean with is_disposable and category.

Algorithmic suggestions never fire for a domain that resolves. If MX records exist, mail can be delivered there, and only the hand-verified list above may override that. Suggestions are drawn from a curated few hundred well-known domains rather than the full provider list, because matching against thousands produces confident nonsense. Treat the result as a prompt for your user, never an automatic correction.

domain.registration

When the domain was registered, when its record last changed, when it changed hands, and when it expires — read over RDAP.

registered_on string
Date the domain was first registered, as YYYY-MM-DD.
updated_on string
Date the registry record last changed. Often the most telling of the four — see below.
transferred_on string
Date the domain last changed hands, where the registry publishes it. Many don't, so null proves nothing.
expires_on string
Date the current registration lapses if not renewed.
age_days number
Whole days since registered_on, derived rather than reported. null whenever registered_on is.

Dates, never judgements. There is no "is this domain new" boolean, because the question you probably want answered — is this domain new to its current owner — is not observable:

  • Renewal doesn't reset the creation date. A domain renewed yesterday still reports the day it was first registered.
  • A registrar transfer doesn't either. The domain carries its original date across.
  • Expiry then re-registration does. If a domain drops and someone else catches it, you get a brand-new creation date — which is correct, since it is a new registration by a new owner.
  • A transfer is sometimes visible. transferred_on carries it where the registry publishes one — a .io domain registered in 2020 and moved in 2024 reports both dates. Plenty of registries omit it, so its absence proves nothing.

Which is why updated_on and transferred_on ship alongside and matter as much: a 2020 creation date next to a 2024 transfer tells a story neither tells alone, and a decades-old domain whose record changed three days ago tells another. Weigh them yourself.

Nulls are common and don't mean "new". Three separate reasons produce one, and the response can't tell them apart: the registry publishes no RDAP at all, it publishes but redacts the dates (.de gives only updated_on), or the lookup didn't answer inside its budget — this is the slowest call in the request and is abandoned rather than allowed to hold the response open. A null means "not available", never "recently registered".

Coverage is uneven because of the wider ecosystem, not by choice here. RDAP is mandatory for generic TLDs (.com, .net, .org and the rest), and those are reliable. Country TLDs are run by sovereign registries and many never implemented it — .jp, .it, .es and .vn have no server to ask, while .ru, .cn, .co and .eu answer but redact every date. Two dozen country TLDs missing from the public routing registry are reached through their own servers instead, so .io, .us, .ch and others do resolve.

Don't need it? response_fields_omit=domain.registration skips the call entirely rather than just hiding its result.

gravatar

Whether the address has a Gravatar, and where to find it.

exists boolean
Whether a Gravatar is registered for the address.
avatar_url string
URL of the image, or null when there isn't one.
profile_url string
URL of the public Gravatar profile, or null.

Read a true; ignore a false. Present means a real person registered that address with Gravatar or WordPress, which is decent evidence the address is real and in use. Absent means almost nothing — the overwhelming majority of working addresses have never had one.

This discloses the address to a third party. The check sends a hash of the address to Gravatar, and a hash of an email is not anonymous — common addresses are trivially reversible. Consider whether your own privacy policy covers that before running user-supplied addresses through this endpoint. If it doesn't, response_fields_omit=gravatar skips the check entirely rather than just hiding its result.

attributes

is_valid_syntax boolean
Whether the address is shaped like an email address. This gates everything else: when it's false, no DNS lookups run at all and every other leaf ships at its default, with the address echoed back so you can see what was judged. The request still returns 200 — a malformed address is a real answer about a real input, not a client error.

It does not mean deliverable. A true says the string is well formed and nothing more — the mailbox may not exist and the domain may not accept mail. This is the most common misreading in this category, which is why the field is named for syntax rather than validity.

The rules are pragmatic rather than a literal RFC 5322 implementation, and biased permissive: wrongly rejecting a working address is far worse than passing an odd one through to the DNS stage, which judges it on its own merits anyway. Internationalised addresses and IDN domains pass.
is_free boolean
Whether the domain belongs to a provider handing out mailboxes at no cost. Matched against the domain itself and nothing else — deliberately not inferred from MX records, because a company running its own domain on Google Workspace or Microsoft 365 has exactly the MX records a free mailbox has while being a paid seat. Inferring would mark most corporate email as free.
is_disposable boolean
Whether the address is a throwaway. Two tests: the domain itself, and failing that, its mail exchangers. The second catches what a domain list can't — throwaway services let anyone point a brand-new custom domain at their mail servers, so the domain is on nobody's list while the mailbox behind it is as disposable as the service's own. Each exchange is walked up its own labels (prd-smtp.10minutemail.com, then 10minutemail.com), stopping before a bare TLD.

The MX half is inference, not fact, and inherits the DNS lookup's limits: a domain whose records couldn't be resolved is judged on its name alone. Every disposable domain is also a free one, so a true here means is_free is true as well.
is_forwarder boolean
Whether the address relays to a mailbox somewhere else rather than terminating where it points. Same two tests as is_disposable — the domain itself, then its mail exchangers — against a list of relay and alias services. The MX test carries most of the weight here: alias services are caught by their own domains (duck.com, mozmail.com), but the larger population is people pointing a domain they own at a forwarding service, and those domains are unlistable by nature.

A false here is weak evidence. A vanity domain quietly forwarding to someone's mailbox through its host's ordinary mail service is indistinguishable from any other hosted domain, so no amount of curation reaches it. Errors are always false negatives; a true is good evidence, a false is not.
is_likely_role_based boolean
Whether the address belongs to a function rather than a person — support@, billing@, postmaster@. The only attribute read from the local half rather than the domain, so it asks nothing of DNS: [email protected] and [email protected] are both role-shaped.

Matched against parts.username, so a tagged role address (support+ticket-4417@) still counts, and matched case-insensitively. Exact matches only — never substrings, so supported@ and sandra@ are not caught by support and sand.

Likely is doing real work in the name. The list is curated to exclude anything a person might plausibly choose as their own local part — ordinary given names like dev and dean, and two-letter forms like it that collide with initials — so a true is strong evidence. But a company small enough to route hello@ to one founder's inbox makes it a personal mailbox in every sense that matters, and nothing observable says so.
has_plus_addressing boolean
Whether the local part carries a plus-addressing tag, i.e. whether parts.tag is non-null. A trailing + with nothing after it counts — the + is there either way.

This is a syntactic answer, not a provider-aware one. It says the local part is tagged, not that the mailbox's provider implements tagging: + is a legal local-part character, and a provider that doesn't support the feature treats one as an ordinary part of the mailbox name. Note also that providers disagree on the delimiter — some use - — and only + is detected here.

Freshness

Every lookup is resolved from its source and then reused briefly, so a domain asked about twice in quick succession costs one round trip rather than two. All of it is a read of somebody else's published data — DNS records and registry entries — and none of it is stored beyond the windows below.

Field Reused for Keyed by
domain.mx 1 hour domain
domain.authentication 1 hour domain
gravatar 1 hour address
domain.registration 7 days domain

The windows track how fast each thing actually changes. A provider's mail servers and sending policy change when somebody reconfigures them, not on a schedule — gmail.com's haven't moved in years. Registration dates are historical facts and cannot change at all.

Everything read from the address itself — parts, tld, attributes, category and did_you_mean — is computed per request and is never stale.

Because caching is keyed by domain, one address warms the lookups for every other address at the same domain. Only gravatar is per-address.

Errors

Failures come back under a top-level errors key as an array of objects, each carrying a message. Shared authentication and rate-limit failures are documented in the main reference; the one specific to this API is below.

Status When
400 No address was supplied — neither email nor q. Unlike the IP API, which falls back to the caller's own address, there is nothing sensible to substitute here.

Note that a malformed address is not an error. It comes back 200 with is_valid_syntax false, the address echoed, and every other leaf at its default — a real answer about a real input rather than a client error. Only a missing address is a 400.

Gotchas

Encode the + in plus-addressed aliases

In a query string a bare + is the legacy encoding for a space, so [email protected] can arrive as someone [email protected] and parse into the wrong username. Percent-encode it as %2B (?email=someone%[email protected]) and the tag survives. Any standard URL builder does this for you; it's hand-written URLs that trip on it.

CORS & browser origins

Calling this API from frontend JavaScript is blocked unless the page's hostname is listed on the API key's allowed origins, so a key that works from your server will fail in the browser until you add it. Requests that carry no Origin header (server-to-server calls, curl) are never affected. Add hostnames on the API Keys page, and see Browser usage & CORS for the matching rules.