How to find the company behind an email address
· 7 min read · Business email
To find the company behind an email address, take everything after the @, reduce it to the registrable domain (mail.acme.co.uk becomes acme.co.uk), throw away domains that can't identify an employer (Gmail, Outlook, disposable services, consumer ISPs), and then resolve the remaining domain to an organization by reading its website or calling an enrichment API. What you get back is the organization that controls the mail server. That's the employer most of the time, and the exceptions are predictable enough to handle in code.
This post covers each step, the shortcuts that don't work, and a small Node.js implementation you can run without any paid data source.
Step 1: Extract and normalize the domain
The domain part of an address is case-insensitive, may contain a subdomain, and may be an internationalized name. Three things to do before anything else:
- Lowercase it and split on the last
@(quoted local parts can legally contain@). - Convert Unicode domains to punycode so
müller.deandxn--mller-kva.dehit the same cache row. - Collapse to the registrable domain using the Public Suffix List. This is what turns
eu.mail.acme.co.ukintoacme.co.ukand keepsox.ac.ukfrom collapsing intoac.uk.
Step 3 is where hand-rolled code breaks. "Take the last two labels" gives you co.uk for every British company. We wrote a whole post on why mail.stripe.com is stripe.com; the short version is to use a library that ships the PSL.
// npm install tldts
import { parse } from 'tldts';
export function emailDomain(email) {
const at = email.lastIndexOf('@');
if (at < 1) return null;
let host;
try {
// The URL parser lowercases and punycodes IDNs for us.
host = new URL(`http://${email.slice(at + 1).trim()}`).hostname;
} catch {
return null;
}
const { domain, isIcann } = parse(host);
return domain && isIcann ? domain : null;
}
emailDomain('Jane.Doe@EU.Mail.Acme.co.uk'); // 'acme.co.uk'
emailDomain('ana@müller.de'); // 'xn--mller-kva.de'
Step 2: Discard domains that can't name an employer
Most lookups fail here, and it's cheap to catch them early. A domain only points to a company if the company controls who gets an address on it. Plenty of domains don't work that way:
| Domain type | Examples | What the domain tells you |
|---|---|---|
| Free webmail | gmail.com, outlook.com, yahoo.co.uk, gmx.de | Nothing about the employer |
| Disposable | mailinator.com, guerrillamail.com | Nothing, and the address may not exist tomorrow |
| Consumer ISP | comcast.net, btinternet.com, orange.fr | Who sells the person broadband, not who employs them |
| Educational | harvard.edu, ox.ac.uk | An institution, but the person may be a student, staff member or alum |
| Government | gov.uk, gouv.fr subdomains | A public body; usually not something you'd "enrich" like a startup |
| Everything else | stripe.com, acme.io | Probably an organization. Go to step 3. |
ISP domains are the trap people miss. Run comcast.net through a naive enrichment pipeline and you'll get Comcast's corporate profile, and a retiree asking about your pricing page lands in the enterprise sales queue. The ISP category exists for exactly that reason. If you're building the business-vs-personal split yourself, telling a business email from a personal one covers the lists and their limits.
Step 3: Resolve the domain to an organization
You have a domain that probably belongs to an organization. Now you need a name, and ideally a bit more. Here's how the usual sources compare:
| Source | Gives you | Reliability |
|---|---|---|
| WHOIS / RDAP | Registrant org, sometimes | Low. Registrant names are usually redacted for gTLDs since 2018, and privacy proxies are common. |
| MX records | The mail provider (Google, Microsoft) | Useless for identity. Half the internet is on the same two providers. |
| The homepage | Title, og:site_name, schema.org markup |
Good for the name. Free. Breaks on parked domains and JavaScript-only sites. |
| Enrichment API | Name plus industry, size, location, socials | Good for well-known companies, weaker for small ones. Costs money or rate limit. |
| Manual search (LinkedIn, Google) | Everything | Accurate, slow, doesn't scale. |
For a free, automated first pass, the homepage is the best source. Many sites embed a schema.org Organization object in JSON-LD for search engines, which hands you the legal-ish name directly. When that's missing, og:site_name and the <title> tag are decent fallbacks.
const ORG_TYPES = /Organization|Corporation|LocalBusiness|NGO|EducationalOrganization/;
const flatten = (d) =>
Array.isArray(d) ? d.flatMap(flatten)
: d && d['@graph'] ? flatten(d['@graph'])
: [d];
export async function nameFromHomepage(domain) {
let res;
try {
res = await fetch(`https://${domain}`, {
redirect: 'follow',
signal: AbortSignal.timeout(5000),
headers: { 'user-agent': 'company-lookup/1.0' },
});
} catch {
return null; // DNS failure, TLS error, timeout
}
if (!res.ok) return null;
const html = (await res.text()).slice(0, 500_000);
// 1. schema.org JSON-LD: the most deliberate signal a site gives you.
const ldBlocks = html.matchAll(
/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi,
);
for (const [, json] of ldBlocks) {
try {
for (const node of flatten(JSON.parse(json))) {
const types = [].concat(node?.['@type'] ?? []);
if (node?.name && types.some((t) => ORG_TYPES.test(t))) {
return { name: node.name, source: 'json-ld', finalUrl: res.url };
}
}
} catch { /* malformed JSON-LD is common; keep going */ }
}
// 2. Open Graph site name.
const og = html.match(
/<meta[^>]+property=["']og:site_name["'][^>]+content=["']([^"']+)["']/i,
);
if (og) return { name: og[1].trim(), source: 'og', finalUrl: res.url };
// 3. <title>, trimmed at the first separator ("Acme | Invoicing for agencies").
const title = html.match(/<title[^>]*>([^<]+)<\/title>/i);
if (title) {
const name = title[1].split(/\s[|:·-]\s/)[0].trim();
if (name) return { name, source: 'title', finalUrl: res.url };
}
return null;
}
Keep the source field. A JSON-LD name was put there on purpose; a title-tag guess deserves less trust. Also keep finalUrl: if acme-mail.com redirects to acme.com, that redirect is itself evidence of who owns the mail domain.
What this won't give you is anything beyond a name. Industry, headcount and location need either more scraping (about pages, footers, LinkedIn) or a data source that has already done it.
Where "the company behind the domain" goes wrong
Even with a clean domain and a correct name, the answer can be misleading. These cases show up in every real dataset:
- Vanity and personal domains.
jane@janedoe.devis a company domain by every mechanical test. The homepage is a portfolio. If your downstream logic assumes "company domain means B2B lead with a buying committee", this person breaks it. - Subsidiaries and parents. Acquired companies often keep their mail domain for years. The domain resolves correctly to the subsidiary, but your sales team may care about the parent. Ask for the parent explicitly if it matters.
- Contractors and agencies. Consultants are often issued an address on the client's domain. The domain is correct about the organization, wrong about the employer.
- Parked or redirected domains. A domain with mail but no website, or one that redirects to a registrar's parking page, gives you no name at all. That absence is a signal in itself, usually worth more to a fraud model than to a sales team.
- Shared university and government domains. One domain covers thousands of departments. The organization is right; it just isn't very useful.
None of these need special code. They need you to treat the result as "the organization that issues this mailbox" and to store how confident you are.
Doing it with one API call
If you'd rather not run the scraper above, pingutil does steps 1 to 3 in one request: it normalizes to the registrable domain, categorizes it (webmail, disposable, ISP, education and government all come back with company: null), and for company domains returns a structured profile filled by an LLM and cached per domain.
export async function companyFromEmail(email) {
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 }),
});
if (!res.ok) throw new Error(`lookup failed: ${res.status}`);
const data = await res.json();
if (data.category !== 'company' || !data.company) {
// free, disposable, isp, educational, government, or enrichment unavailable
return { domain: data.domain, category: data.category, company: null };
}
const c = data.company;
return {
domain: data.domain,
category: 'company',
company: {
name: c.name,
website: c.website,
industry: c.industry,
size: c.employee_size_estimate,
country: c.headquarters?.country ?? null,
parent: c.parent_company,
linkedin: c.linkedin_url,
},
// The model's own estimate. Below ~0.6, show it to a human before acting on it.
confidence: c.confidence,
stale: data.meta.served_stale === true,
};
}
Two things to know before relying on it. First-seen domains take a few seconds because of the LLM call; repeat lookups come from cache. And confidence is the model's self-assessment, so a small agency with a generic name can come back confidently wrong. We wrote up how accurate LLM-based enrichment actually is, including where it fails. Field details are in the API reference.
A checklist for your own implementation
- Split on the last
@, lowercase, punycode, collapse to eTLD+1 with the Public Suffix List. - Short-circuit webmail, disposable, ISP, education and government domains before any network call.
- Resolve the rest from the homepage (JSON-LD, then
og:site_name, then<title>) or an enrichment API. - Store the source and a confidence level next to the company name, and cache per registrable domain, not per email.
- Never write "employer" in your UI. Write "organization", and let a human confirm anything that triggers money or sales effort.
If the next step is turning that company into a routed, scored lead, enriching inbound leads from an email domain picks up where this leaves off.
Questions people ask
- Can I find someone's employer from a Gmail address?
- Not from the address itself. gmail.com, outlook.com and other webmail domains are shared by millions of unrelated people, so the domain says nothing about an employer. You have to ask the user, or match the person against another data source.
- Does WHOIS tell you which company owns a domain?
- Rarely, these days. Since GDPR took effect in 2018, registrars redact the registrant's name for most generic TLD domains in public WHOIS and RDAP output, and many companies use privacy proxies anyway. The website on the domain is a far better source.
- Is the company behind an email domain always the person's employer?
- Usually, but not always. Contractors are often given client addresses, subsidiaries keep their own domains, and a one-person vanity domain looks exactly like a company domain. Treat the result as the organization that controls the mailbox, not as proof of employment.