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/jsonon every response. Successful payloads sit under a top-leveldatakey with the record indata.objects; failures come back undererrorsas an array of objects carrying amessage. -
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();
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
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
Query parameters
Every parameter below is optional unless it carries a required badge.
email
string
required
q; email wins when
both are present. A request naming neither is rejected with a
400 — see
Errors.
response_fields
string
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
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) and truthy values
(true, 1)
enable; anything else (including
false and 0)
leaves the response compact.
Example
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)
{
"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
email
parameter, then the q alias.
null when neither was supplied.
did_you_mean
string
null otherwise — which is most of the
time. See did_you_mean.
parts
object
tld
object
domain
object
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
attributes
object
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
@, tag included.
username
string
local when there's no tag.
tag
string
+, 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
@, 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
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
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
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 |
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.
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
priority
number
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
spf.policy
string
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
dmarc.policy
string
none,
quarantine or
reject, straight from the record's
p= tag.
dmarc.subdomain_policy
string
sp= tag.
null when the record doesn't set one, in
which case subdomains inherit
dmarc.policy.
dmarc.percentage
number
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.
<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.
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
YYYY-MM-DD.
updated_on
string
transferred_on
string
null proves nothing.
expires_on
string
age_days
number
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_oncarries it where the registry publishes one — a.iodomain 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
avatar_url
string
null when there isn't
one.
profile_url
string
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.
response_fields_omit=gravatar skips the check
entirely rather than just hiding its result.
attributes
is_valid_syntax
boolean
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
is_disposable
boolean
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
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
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
+ 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.