Skip to content

The domain lookup

Terminal window
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.

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: true and X-Credits-Charged: 0 - even when the balance is now empty.
  • The same key and a different domain is 409 idempotency_conflict. Example.COM, example.com and example.com. are one domain for this purpose.
  • The same key while the first attempt is still running is 409 request_in_progress with Retry-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.

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.

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.

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.

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.

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.

Terminal window
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.

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.