The domain lookup
curl -sS https://api.emvero.tech/v1/domain/example.com \ -H "Authorization: Bearer emv_your_credential" \ -H "Idempotency-Key: 3f0c9a52-6d1e-4b8e-a7f4-2c51d0e9b6aa"{ "schema_version": "1", "operation_id": "45eb36a9-b4e6-48ac-94a9-0ad145b6cf98", "domain": "example.com", "ascii_domain": "example.com", "status": "complete", "generated_at": "2026-09-18T10:00:00Z", "mail_routing": { "status": "routable", "method": "mx", "issues": [], "meta": { "status": "ok", "reason": null, "source": "dns", "source_version": null, "observed_at": "2026-09-18T10:00:00Z", "expires_at": "2026-09-18T10:05:00Z", "stale": false } }, "mx": { "records": [{ "host": "mx.example.com", "priority": 10, "provider_id": null }], "provider_id": null, "provider_match": "unknown", "truncated": false, "meta": { "status": "ok", "reason": null, "source": "dns", "source_version": "catalog:17", "observed_at": "2026-09-18T10:00:00Z", "expires_at": "2026-09-18T10:05:00Z", "stale": false } }, "email": { "classification": { "disposable": false, "public_provider": false, "meta": { "status": "ok", "reason": null, "source": "dataset", "source_version": "bundle:example-v1", "observed_at": null, "expires_at": null, "stale": false } }, "catch_all": { "state": "unknown", "provider_id": null, "meta": { "status": "unknown", "reason": "not_observed", "source": null, "source_version": null, "observed_at": null, "expires_at": null, "stale": false } } }, "email_authentication": { "spf": { "present": true, "syntax": "valid", "record": "v=spf1 -all", "all_qualifier": "fail", "includes": [], "redirect": null, "evaluation": "not_performed", "issues": [], "meta": { "status": "ok", "reason": null, "source": "dns", "source_version": "spf-syntax:1", "observed_at": "2026-09-18T10:00:00Z", "expires_at": "2026-09-18T10:05:00Z", "stale": false } }, "dmarc": { "present": true, "syntax": "valid", "record": "v=DMARC1; p=reject", "policy_domain": "example.com", "inherited": false, "published_policy": "reject", "effective_policy": "reject", "testing": false, "adkim": "r", "aspf": "r", "evaluation": "not_performed", "issues": [], "meta": { "status": "ok", "reason": null, "source": "dns", "source_version": "dmarc-rfc9989:1", "observed_at": "2026-09-18T10:00:00Z", "expires_at": "2026-09-18T10:05:00Z", "stale": false } } }, "network": { "mail": [ { "host": "mx.example.com", "priority": 10, "addresses": [ { "ip": "198.51.100.25", "asn": 64500 }, { "ip": "203.0.113.25", "asn": 64511 }, { "ip": "2001:db8::25", "asn": 64500 } ] } ], "web": { "host": "example.com", "cname_chain": [], "addresses": [{ "ip": "198.51.100.80", "asn": 64500 }, { "ip": "2001:db8::80", "asn": 64500 }], "truncated": false, "edge_provider": null, "meta": { "status": "ok", "reason": null, "source": "dns", "source_version": null, "observed_at": "2026-09-18T10:00:00Z", "expires_at": "2026-09-18T10:05:00Z", "stale": false } }, "autonomous_systems": [ { "asn": 64500, "handle": "EXAMPLE-ISP", "name": "Example Access", "country": "DE", "type": "isp" }, { "asn": 64511, "handle": null, "name": null, "country": null, "type": "unknown" } ], "meta": { "status": "ok", "reason": null, "source": "dataset", "source_version": "20260918T003516Z+0ba445c4bdda", "observed_at": "2026-09-18T00:35:16Z", "expires_at": null, "stale": false } }, "registration": null, "billing": { "product": "domain-intelligence", "variant": "standard", "operation_credits": 1 }}The example is synthetic. Every field in it is always present: a value nobody
could establish is null or an empty list, never a missing key, so a client can
read the shape without guarding each step.
What it costs
Section titled “What it costs”One credit for a complete lookup, two for a complete
extended one, nothing for anything
less. The credits are the same ones a verification spends - there is one
balance per account, and Credits has the rest.
| What came back | standard |
extended |
|---|---|---|
"status": "complete" - everything this lookup covers was established fresh |
1 credit | 2 credits |
"status": "partial", with only the registration date missing |
- | 1 credit |
"status": "partial" otherwise - the mail routing is known, something secondary is not |
free | free |
| A repeat of an earlier lookup with the same key | free | free |
| Any refusal | free | free |
A complete answer can still say something unwelcome. A domain that publishes
MX 0 ".", a domain that does not exist, a routing that is misconfigured - each
is a finished answer at the whole price, for the reason a verification charges
for a domain that does not exist: what is sold is the decision.
The key is required, and it is what makes a retry safe
Section titled “The key is required, and it is what makes a retry safe”Idempotency-Key is mandatory here, unlike on a verification. One key, 1 to
128 characters from A-Z a-z 0-9 . _ : -; a UUID is the easy choice.
- The same key and the same domain within 24 hours of completion returns
the saved answer byte for byte, with
X-Idempotent-Replay: trueandX-Credits-Charged: 0- even when the balance is now empty. - The same key and a different domain is
409 idempotency_conflict.Example.COM,example.comandexample.com.are one domain for this purpose. - The same key while the first attempt is still running is
409 request_in_progresswithRetry-After: 1. Wait and send it again.
A replayed body keeps billing.operation_credits at what the original cost,
while X-Credits-Charged says what this attempt cost. The body describes the
operation; the header describes the request.
When a lookup’s charge could not be confirmed, it answers
503 commit_unknown (or accounting_uncertain) and omits X-Credits-Charged
and the balance. It may already have been
charged. Retry with the same key: the retry reaches the same operation and
either returns its saved answer or finishes it, and it never charges twice. A
new key is a new lookup.
Reading the answer
Section titled “Reading the answer”mail_routing - can the domain receive mail at all
Section titled “mail_routing - can the domain receive mail at all”status |
Meaning |
|---|---|
routable |
Mail has somewhere to go, through MX records (method: "mx") or, with none published, the domain’s own address ("implicit"). |
no_mail |
The domain exists and publishes nothing that routes mail. |
null_mx |
The domain publishes MX 0 "." - it has said in public that it accepts no mail. |
nxdomain |
The domain does not exist. |
misconfigured |
What is published cannot route mail, for example a null MX mixed with real ones. issues says what. |
unknown |
The routing could not be established. Only a partial answer carries this. |
routable says mail has a route, not that any recipient will be accepted.
Nothing here opens an SMTP session - that is what a
full verification is for.
If the routing itself cannot be established, there is no answer: the lookup
refuses with 503 source_unavailable, free, rather than return a guess. A DNS
failure is never reported as a domain without mail.
mx - who receives it
Section titled “mx - who receives it”records are sorted by priority, then host. provider_id names the operator
the most preferred records (the lowest priority number) point at when they all
agree
(provider_match: "matched"); mixed means they point at different operators,
and unknown that at least one is not one we recognise. The operator is who
takes the mail in, which can be a filtering gateway in front of the mailbox
host rather than the host itself. truncated: true means more records were
published than one lookup reads, and the answer is partial.
email.classification and email.catch_all
Section titled “email.classification and email.catch_all”disposable and public_provider are the same signals the
disposable and public-provider
lookups answer, from the same list. false means the domain is not on the list,
which is not proof of the opposite.
catch_all.state is the catch-all state this service
holds for the domain: yes, no or unknown. For a domain that cannot receive
mail it stays unknown and its meta.status says not_applicable. It is the
state verification already knows. This
lookup never probes a mail server to establish one, so unknown is common and
means exactly that.
email_authentication - what the domain publishes
Section titled “email_authentication - what the domain publishes”SPF and DMARC are the records as published, parsed and checked for syntax.
present is true, false, or null when it could not be read. For DMARC,
published_policy is what the record says and effective_policy is what it
comes to - a record in testing mode (t=y) asking for reject applies
quarantine. inherited: true means the policy was found at an organisational
domain above this one, named in policy_domain.
evaluation is always not_performed. No message is authenticated here -
there is no message. Whether a given sender would pass is a question about a
message this service never sees. DKIM keys are not discovered, and SPF
includes are listed rather than followed.
network - which networks hold the mail and the site
Section titled “network - which networks hold the mail and the site”network.mail lists each mail host the routing reached, with the public
addresses it resolved to. network.web is the requested name’s own A and
AAAA records and the CNAMEs followed to them; www. is not added, and an
address does not mean a web server answers there. Every address names asn,
the autonomous system announcing it in the global routing table, or null when
none does.
autonomous_systems describes each of those systems once: its handle, its
name, the country it is registered in - not where its servers stand - and
its type: isp, hosting, business, education_research,
government_admin or unknown. The system is the one announcing the route,
which is not always the brand a reader expects.
A CDN is hosting. When the name is behind Cloudflare, Fastly, Akamai, Imperva,
CloudFront or Azure, web.edge_provider names it: the addresses are then the
edge’s, and the origin behind it is unknown.
None of this decides the charge. network.meta describes the routing dataset
the attribution comes from - rebuilt daily from public routing data, and
observed_at is when - and a failure here leaves a complete answer complete.
meta, on every part
Section titled “meta, on every part”Each part carries where it came from (source: dns, dataset, registry,
probe, operator, provider, derived), which version of our data answered
(source_version), when it was observed and until when it holds, and whether a
previously good value is being shown past a failed refresh (stale). reason
explains an absent value - not_observed, dns_timeout, source_unavailable
and similar. New reasons and issue codes may appear; treat one you do not
recognise as information, not as an error.
Asking for a domain
Section titled “Asking for a domain”One domain per request, in the path, with no body. The one query parameter is
variant: standard, the default, or extended; any other is
400 unsupported_parameter. An internationalised name is accepted and
converted, and comes back in both forms.
A subdomain is looked up as itself, not as its parent: mail.example.com has
MX records of its own, or none.
Sending an address, a URL, an IP address, a wildcard or a port is
400 invalid_domain. Unlike the free lookups, this one does not accept an
address and discard the local part - the domain is the resource.
The registration date: variant=extended
Section titled “The registration date: variant=extended”curl -sS "https://api.emvero.tech/v1/domain/github.com?variant=extended" \ -H "Authorization: Bearer emv_your_credential" \ -H "Idempotency-Key: 8c1d2e7a-4b9f-4f0e-9a3c-61e5d2b7c904"An extended lookup is the whole standard answer plus registration, what the
domain’s registry publishes about it over RDAP:
"registration": { "registered_domain": "github.com", "registered_at": "2007-10-09T18:20:50Z", "age_days": 6932, "expires_at": "2028-10-09T18:20:50Z", "registrar": "MarkMonitor Inc.", "statuses": ["client delete prohibited", "client transfer prohibited", "client update prohibited"], "meta": { "status": "ok", "reason": null, "source": "registry", "source_version": "rdap.verisign.com", "observed_at": "2026-10-02T17:54:45Z", "expires_at": "2026-11-01T17:54:45Z", "stale": false }}registered_domain is where the registry keeps the record: the name under its
public suffix, so mail.example.co.uk is answered for example.co.uk.
age_days is whole days from registered_at to generated_at - the age of the
registration, not of the company, the site or whoever holds the domain now. A
standard lookup carries "registration": null.
An answer is reused: meta.observed_at is when the registry was asked and
meta.expires_at until when its answer is reused - a month for a registration
older than a year, a day otherwise.
When there is no date, registration.meta says why. The lookup is then
partial, at one credit if everything the standard depth covers was
established:
meta.status |
reason |
Meaning |
|---|---|---|
not_applicable |
unsupported_tld |
The registry publishes no RDAP. Many country domains, among them .io, .ru, .de and .co. |
not_applicable |
not_found |
The registry has no such domain. |
ok |
missing_field |
The registry answered without a registration date. |
unknown |
pending |
The registry had not answered within the lookup’s time. |
unknown |
rate_limited |
The registry asked us to slow down. |
error |
registry_error, source_unavailable |
The registry, or our side, failed. |
A pending date usually arrives within seconds and is then reused. The key
replays the saved partial answer, so asking again means a new key - and a
new lookup at its own price. A key is also bound to its variant: the same key
with the other one is 409 idempotency_conflict.
Errors
Section titled “Errors”| Status | error |
What to do |
|---|---|---|
400 |
invalid_domain, invalid_idempotency_key, unsupported_parameter, unexpected_body |
Fix the request. It will not succeed on retry. |
401 |
unauthorized |
See Authentication. |
402 |
insufficient_credits |
Top up. A replay of a finished lookup is still answered at zero balance. |
403 |
delegation_not_permitted, billing_context_forbidden |
The request tried to bill an account this credential cannot bill. Ordinary credentials bill their own account; remove whatever named another. |
405 |
method_not_allowed |
Use GET. |
409 |
idempotency_conflict, request_in_progress |
A different domain under the same key, or the first attempt is still running - see above. |
429 |
rate_limited, new_domain_rate_limited, processing_limit |
Back off; honour Retry-After. See Limits. |
503 |
product_unavailable, service_unavailable, capacity_exceeded, authentication_unavailable, source_unavailable, dataset_unavailable, catalog_unavailable, resource_limit, accounting_paused |
Retry with backoff; honour Retry-After where present. Nothing was charged. |
503 |
commit_unknown, accounting_uncertain |
Retry with the same key. It may already be charged, and the retry will not charge again. |
Every response, errors included, carries Cache-Control: private, no-store.
The answer belongs to the account that paid for it, and no shared cache between
you and us should keep a copy.
When to use this, and when to use something else
Section titled “When to use this, and when to use something else”Use this to see how a domain handles mail: before onboarding a customer
domain, when a deliverability question lands on your desk, when auditing which
domains in a list publish DMARC. It is one request and one credit for the whole
picture. Add variant=extended when the age of the domain matters - a domain
registered last week behind a new signup is worth a second look.
Use the free lookups when the only question is whether a domain is a throwaway or a public mailbox provider.
Use a verification when the question is about an address. A
domain-depth check returns a verdict about whether mail can reach the domain,
and a full one asks the destination about the recipient. This lookup does
neither.
The same lookup is available without writing code at emvero.io/account/domain/, for the signed-in account.