Signup domains with no website: what it tells you
· 6 min read · Email validation
When someone signs up with an address on a domain that can receive email but has no website, it usually means the domain exists for mail only. Sometimes that's a person's vanity domain or a company's separate mail domain. Sometimes it's a domain registered last week to receive confirmation emails for an abuse campaign. On its own it is a moderate risk signal worth scoring, not blocking. Paired with a very young registration date and a random-looking name, it becomes one of the stronger cheap signals you can get.
What "no website" means at the DNS level
A website needs an address for the browser to connect to. For a domain to "have a website" in the most basic sense, at least one of these must resolve:
- an
AorAAAArecord on the apex (example.com) - an
AorAAAArecord onwww.example.com(often reached via aCNAME)
Receiving mail needs something different: an MX record, or under RFC 5321's implicit-MX rule, an address record on the domain itself. So it is entirely possible, and cheap, to have a domain that accepts mail and serves nothing on the web. Just publish an MX record pointing at a mail host and nothing else.
That gives you a small truth table worth memorising:
domain_exists |
can_receive_email |
has_website |
Typical reality |
|---|---|---|---|
| false | false | false | Typo, expired, or made-up domain. The address can't work. |
| true | false | false | Registered but unused, or parked with no DNS. Address can't receive. |
| true | false | true | Website on www only, no MX. Rare, because an apex A record would count as an implicit MX. |
| true | true | false | Mail-only domain. The interesting case. |
| true | true | true | Normal. Says nothing either way. |
The first three rows are easy calls: the user can't receive your confirmation email, so tell them to check the address. The fourth row is where judgment comes in. The DNS mechanics behind the first two columns are covered in checking MX records.
Why abusers end up in the mail-only row
Think about what someone running a signup farm on their own domain needs. They need your confirmation emails to arrive, so they need MX. They don't need a website, because nobody visits it. Setting up a site is extra work for zero benefit to them. So a domain bought for abuse, left at default settings plus one MX record, lands exactly in row four.
It gets more telling with two more facts:
- Registration date. A domain registered days before the signup, with no website, receiving mail, is a pattern that honest users rarely produce. A 10-year-old mail-only domain is almost certainly someone's personal address.
- The name itself. Bulk-registered abuse domains often have random-looking labels. A pronounceable brand name is weak evidence the other way.
The innocent mail-only domains
Before you build a rule, know who else lands in row four:
- Personal vanity domains. Lots of people own
theirname.comortheirname.devpurely for email. Several mainstream mail providers make custom domains easy, and the owner may never set up a site. - Separate corporate mail domains. Some organisations send mail from a domain that isn't their marketing site, for brand, legacy or merger reasons.
- Pre-launch companies. A three-person startup that registered its domain last month may have MX configured before it has a landing page. Young, mail-only, and a perfectly good customer.
- Website on a different host name. A site that only exists at
app.example.comorshop.example.comwill look website-less to an apex-plus-www check.
The last two are why "no website" should never block a B2B signup by itself. They're also why combining it with domain age matters so much: it separates the 10-year-old vanity domain from the 3-day-old one.
What DNS can't tell you
Two blind spots cut in opposite directions.
Parked domains look like they have a website. A registrar's parking page is served from a real IP, so the apex resolves and a DNS check says has_website: true. If you need to distinguish "real site" from "for sale" page, you need an HTTP fetch and some content heuristics, which is slower and more fragile. Many teams decide it isn't worth it for signups.
A null MX looks like mail capability to a naive check. RFC 7505 defines a "null MX": a single MX record with preference 0 and a target of ., meaning "this domain accepts no mail". A check that only asks "is there any MX answer?" will say yes. pingutil's can_receive_email currently works that way (any MX answer, or an apex A record, counts), so if null-MX domains matter to you, resolve the MX yourself and look for the 0 . record. Doing that from serverless code is covered in DNS-over-HTTPS for domain checks.
Getting the signals in code
pingutil returns all three DNS facts when you pass check_dns: true. The has_website value is the DNS definition above (apex A, www A, or apex AAAA). Results are cached for 7 days, and the field never changes verdict. What you do with it is your policy.
curl -X POST https://pingutil.com/v1/lookup \
-H "Authorization: Bearer pk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "input": "someone@preparmy.com", "skip_enrichment": true, "check_dns": true }'
# "domain_signals": { "domain_exists": true, "can_receive_email": true, "has_website": false }
For registration age, use RDAP, the JSON successor to WHOIS. RFC 9083 defines an events array where the entry with eventAction: "registration" carries the creation date. The public rdap.org bootstrap service redirects a query to the right registry; it allows about 10 requests per 10 seconds, so cache aggressively and consider the IANA bootstrap files for volume.
// Domain age in days via RDAP, or null when the registry has no RDAP service.
async function domainAgeDays(domain: string): Promise<number | null> {
try {
const res = await fetch(`https://rdap.org/domain/${encodeURIComponent(domain)}`, {
headers: { accept: 'application/rdap+json' },
redirect: 'follow',
signal: AbortSignal.timeout(5000),
});
if (!res.ok) return null;
const body = (await res.json()) as { events?: { eventAction: string; eventDate: string }[] };
const reg = body.events?.find((e) => e.eventAction === 'registration');
if (!reg) return null;
return Math.floor((Date.now() - Date.parse(reg.eventDate)) / 86_400_000);
} catch {
return null;
}
}
async function mailOnlyDomainRisk(email: string) {
const res = await fetch('https://pingutil.com/v1/lookup', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.PINGUTIL_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ input: email, skip_enrichment: true, check_dns: true }),
});
const data = await res.json();
const dns = data.domain_signals; // null if the DNS lookup was unavailable
if (data.category !== 'company' || !dns) return { risk: 'n/a' as const };
if (!dns.domain_exists || !dns.can_receive_email) return { risk: 'undeliverable' as const };
if (dns.has_website) return { risk: 'none' as const };
const age = await domainAgeDays(data.domain);
const looksRandom = data.email_signals?.domain_looks_random ?? false;
if (age !== null && age < 30) return { risk: 'high' as const, age, looksRandom };
if (looksRandom) return { risk: 'medium' as const, age, looksRandom };
return { risk: 'low' as const, age, looksRandom };
}
A few things about that function:
- It only bothers with
companydomains. Free webmail, education and government domains are shared infrastructure; whethergmail.comhas a website tells you nothing about the user. - It looks up domain age only in the mail-only case. RDAP calls are the slow, rate-limited part, so spend them where they change the answer.
- The 30-day cutoff is a placeholder. Look at your own labelled abuse cases and pick the number that separates them.
- It uses
data.domain, the registrable domain, souser@mail.newdomain.xyzis checked asnewdomain.xyz. That's also the right key for caching, as discussed in caching domain lookups.
What to do with each answer
| Result | Suggested handling |
|---|---|
undeliverable |
Inline error: "We can't deliver to this domain. Check for typos." |
high (mail-only, registered under ~30 days) |
Allow signup, hold valuable actions (free credits, outbound sending, invites) until extra verification or manual review. |
medium (mail-only, random-looking label) |
Add to the risk score; review if other signals agree. |
low (mail-only, older or unknown age) |
Treat as normal. Probably a vanity or mail domain. |
none |
No signal. |
This plugs in as one feature among several. The email risk score from free signals article shows how to weight it next to disposable domains, randomized local parts and per-domain velocity, and fake signups covers where these accounts come from in the first place.
Questions people ask
- How do I check if a domain has a website?
- The cheap check is DNS: query A and AAAA records for the apex and for www. If neither resolves, there is nowhere for a browser to go. An HTTP request tells you more, such as whether the page is a parking page, but it is slower and easier to fool.
- Is a domain with no website always fake?
- No. Personal vanity domains used only for email, corporate mail domains separate from the marketing site, and pre-launch startups all legitimately lack a website. Treat it as one input to a score, strongest when combined with a very recent registration date.
- How can I find out when a domain was registered?
- Use RDAP, the JSON successor to WHOIS. A domain lookup returns an events array, and the entry with eventAction registration holds the creation date. Some ccTLD registries don't run RDAP, so handle a missing answer.