You have a domain name. Maybe it came from a signup form, a lead list, or a browser tab your sales rep had open. From that single domain, you can pull company name, industry, employee count, funding history, tech stack, and real-time growth signals — in under 500ms. No manual research. No LinkedIn stalking. One API call, and you have a full company profile.
This guide walks you through everything: what domain-based company lookup is, why it matters, how it works under the hood, and how to implement it step by step with production-ready code examples. Whether you're building a lead enrichment pipeline, shortening a signup form, or creating a Chrome extension, this is the technique that powers it all.
What Is Domain-Based Company Lookup?
Domain-based company lookup is simple in concept: you provide a domain name (like stripe.com or notion.so), and an API returns structured company data. Name, industry, headquarters, employee count, revenue range, funding rounds, technologies used, social profiles — the full picture.
The reason domain works so well as a lookup key is that it's the universal company identifier. Think about it:
- Company names are ambiguous. Search for "Mercury" and you'll find a bank, a car brand, a planet, and a NASA program. But
mercury.compoints to exactly one entity. - Domains are unique by definition. DNS guarantees one owner per domain at any point in time. No duplicates, no conflicts.
- You already have the domain. Every email address contains one. Every website visit reveals one. Every form submission includes one. It's the data point you're most likely to already possess.
- Domains are stable. Companies change names, rebrand, merge — but their primary domain tends to persist for years.
google.comhas been the same since 1997.
Compared to matching by company name (which requires fuzzy matching, disambiguation, and still gets it wrong), domain lookup is deterministic. You give it hubspot.com, you get HubSpot. Every time. No guessing.
A domain name is to a company what a Social Security number is to a person — a unique, persistent, and universally available identifier.
The typical flow looks like this:
- User enters an email or URL (e.g.,
[email protected]orhttps://acme.com/about) - You extract the domain (
acme.com) - You call a company data API with that domain
- The API returns structured JSON with 30+ data fields
- You use that data to enrich your records, personalize outreach, or score leads
Use Cases for Domain Lookup
Domain-based company lookup isn't a niche trick. It's the foundation of modern data enrichment. Here are the five most common use cases developers build with it.
1. Form Shortening (Email to Auto-Fill)
Your signup form asks for company name, industry, size, and location. The user fills it in — or more likely, bounces because the form is too long. With domain lookup, you only need one field: work email. From [email protected], you extract the domain, call the API, and auto-fill everything else. Conversion rates go up because friction goes down.
Companies like HubSpot have documented how reducing form fields from 4 to 1 can increase conversion by 50% or more.
2. Lead Enrichment (CRM Integration)
Your sales team imports a list of 500 leads with just names and emails. Before anyone picks up the phone, you run domain lookup on every email domain and populate the CRM with company size, industry, tech stack, and recent funding. Now your reps know if they're calling a 10-person startup or a Fortune 500 before the first ring. This is the core of any enrichment pipeline.
3. Chrome Extension (Tab to Company Card)
Your sales rep is browsing a prospect's website. A Chrome extension reads the current tab's URL, extracts the domain, and displays a floating company card: employee count, industry, last funding round, growth signals. No context switching. No opening another tab to search. The data appears where the rep already is.
4. Security and Compliance (Vendor Assessment)
Your procurement team needs to vet a new SaaS vendor. Instead of sending a questionnaire and waiting two weeks, you run domain lookup on the vendor's website and instantly see company age, employee count, headquarters location (important for GDPR), and technology stack. It doesn't replace a full security audit, but it gives you a first-pass filter in seconds.
5. Analytics (User Segmentation by Company)
Your product analytics show 10,000 active users, but you don't know which companies they belong to. By enriching user email domains, you can segment usage by company size, industry, or geography. Now you know that 40% of your power users come from fintech companies with 50–200 employees — and that's the ICP you should double down on.
How Domain Lookup Works Under the Hood
When you call a domain-based company lookup API, it feels like magic. You send a domain, you get back 30 fields of structured data. But under the hood, there's a multi-layered data resolution pipeline. Here's what happens.
DNS Resolution
The first step is confirming the domain exists and is active. The API performs a DNS lookup to check for A records, MX records, and NS records. This tells you whether the domain is live, whether it handles email, and who hosts it. A domain with no DNS records at all is likely parked or expired. For more on how DNS works, the MDN Web Docs have an excellent primer.
WHOIS Data
WHOIS records contain registration data: when the domain was registered, when it expires, the registrar, and sometimes the organization name. While GDPR has led many registrars to redact personal data, the organizational fields often remain visible for business domains. A domain registered in 2008 tells you something different than one registered last month.
Web Scraping and Structured Data
The API crawls the company's website looking for structured data: meta tags, Open Graph tags, Schema.org markup, and visible text. The "About Us" page, the footer, the careers page — all are signals. A company that lists 50 open positions on their careers page is in growth mode.
ML-Based Entity Matching
Raw signals are noisy. The domain's website might say "Acme Corp" while their LinkedIn says "Acme Corporation" and their Crunchbase profile says "ACME Inc." Machine learning models resolve these entities into a single canonical record. This is where cheap lookup services fall apart — they return whatever they scraped first, while quality APIs like BounceWatch run entity resolution across multiple data sources.
Data Aggregation
Finally, the API aggregates data from multiple sources: government filings, job postings, news articles, social profiles, technology detectors, and proprietary datasets. The result is a unified company profile that no single source could provide alone.
Step-by-Step: Implementing Domain Lookup
Let's build a complete domain lookup integration. We'll cover input parsing, edge cases, the API call, response processing, and caching — with code examples in both Python and JavaScript.
Step 1 — Parse and Clean the Input
Users don't give you clean domains. They give you URLs, email addresses, and everything in between. Your first job is to extract a clean domain from whatever input you receive. Here are the formats you need to handle:
acme.com— already cleanwww.acme.com— strip thewww.https://acme.com/about/team— extract the hostname[email protected]— extract the domain from the emailhttp://subdomain.acme.co.uk/page?ref=123— handle subdomains and country TLDs
Python implementation:
from urllib.parse import urlparse
import re
def extract_domain(input_string: str) -> str:
"""Extract a clean root domain from a URL, email, or raw domain."""
text = input_string.strip().lower()
# Handle email addresses
if "@" in text:
text = text.split("@")[-1]
# Add scheme if missing so urlparse works correctly
if not text.startswith(("http://", "https://")):
text = "https://" + text
parsed = urlparse(text)
hostname = parsed.hostname or ""
# Remove www. prefix
if hostname.startswith("www."):
hostname = hostname[4:]
return hostname
# Examples
print(extract_domain("[email protected]")) # stripe.com
print(extract_domain("https://www.notion.so/")) # notion.so
print(extract_domain("hubspot.com/products")) # hubspot.com
JavaScript implementation:
function extractDomain(input) {
let text = input.trim().toLowerCase();
// Handle email addresses
if (text.includes("@")) {
text = text.split("@").pop();
}
// Add scheme if missing
if (!text.startsWith("http://") && !text.startsWith("https://")) {
text = "https://" + text;
}
try {
const url = new URL(text);
let hostname = url.hostname;
// Remove www. prefix
if (hostname.startsWith("www.")) {
hostname = hostname.substring(4);
}
return hostname;
} catch (e) {
return text; // Return as-is if parsing fails
}
}
// Examples
console.log(extractDomain("[email protected]")); // stripe.com
console.log(extractDomain("https://www.notion.so/")); // notion.so
console.log(extractDomain("hubspot.com/products")); // hubspot.com
For a deeper dive into URL parsing, the MDN URL API documentation covers all the edge cases.
Step 2 — Handle Edge Cases
Before you send a domain to the API, you need to filter out domains that won't return useful company data and handle some tricky patterns.
Free email domains. If someone signs up with [email protected], looking up gmail.com will return Google — not the user's actual company. You need a list of free email providers to skip. We'll cover this in detail in the Free Email Domain Detection section below.
Subdomains. A domain like app.hubspot.com should resolve to hubspot.com. But blog.company.co.uk should resolve to company.co.uk, not co.uk. Handling multi-part TLDs correctly requires a public suffix list. Libraries like tldextract (Python) and psl (Node.js) handle this reliably.
# Python with tldextract
import tldextract
def get_root_domain(domain: str) -> str:
extracted = tldextract.extract(domain)
return f"{extracted.domain}.{extracted.suffix}"
print(get_root_domain("app.hubspot.com")) # hubspot.com
print(get_root_domain("blog.company.co.uk")) # company.co.uk
print(get_root_domain("mail.google.com")) # google.com
Redirects. Some domains redirect to others. fb.com redirects to facebook.com. A good API handles this internally, but if you're building your own resolution layer, follow redirects and use the final destination domain.
Invalid or parked domains. Validate that the domain has active DNS records before calling a paid API. A quick DNS check saves you from wasting lookup credits on dead domains.
Step 3 — Call the API
With a clean, validated domain, you're ready to make the API call. Here's how to call the BounceWatch Company API with proper error handling.
Python:
import requests
API_KEY = "your_api_key_here"
BASE_URL = "https://api.bouncewatch.com/api/v1"
def lookup_company(domain: str) -> dict | None:
"""Look up company data by domain."""
url = f"{BASE_URL}/company/{domain}"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json"
}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.json()
elif response.status_code == 404:
print(f"No company found for domain: {domain}")
return None
elif response.status_code == 429:
print("Rate limit exceeded. Back off and retry.")
return None
else:
print(f"API error {response.status_code}: {response.text}")
return None
except requests.exceptions.Timeout:
print(f"Request timed out for domain: {domain}")
return None
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return None
# Usage
company = lookup_company("stripe.com")
if company:
print(f"Company: {company['name']}")
print(f"Industry: {company['industry']}")
print(f"Employees: {company['employee_count']}")
JavaScript (Node.js):
const API_KEY = "your_api_key_here";
const BASE_URL = "https://api.bouncewatch.com/api/v1";
async function lookupCompany(domain) {
const url = `${BASE_URL}/company/${domain}`;
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${API_KEY}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(10000),
});
if (response.ok) {
return await response.json();
}
if (response.status === 404) {
console.log(`No company found for domain: ${domain}`);
return null;
}
if (response.status === 429) {
console.log("Rate limit exceeded. Back off and retry.");
return null;
}
console.log(`API error ${response.status}: ${await response.text()}`);
return null;
} catch (error) {
console.error(`Request failed for ${domain}:`, error.message);
return null;
}
}
// Usage
const company = await lookupCompany("stripe.com");
if (company) {
console.log(`Company: ${company.name}`);
console.log(`Industry: ${company.industry}`);
console.log(`Employees: ${company.employee_count}`);
}
Notice the explicit handling for 404 (domain not found) and 429 (rate limit). These are the two non-200 responses you'll encounter most often. A production implementation should add exponential backoff for 429 responses. For more on implementing retry logic, Real Python has a thorough guide on the topic.
Step 4 — Process the Response
The API returns a JSON object with dozens of fields. Not all fields will be present for every company — a pre-seed startup won't have public revenue data. Handle nulls gracefully and map the response to your internal data model.
def process_company_response(data: dict) -> dict:
"""Map API response to internal company model."""
return {
"name": data.get("name", "Unknown"),
"domain": data.get("domain", ""),
"industry": data.get("industry", ""),
"sub_industry": data.get("sub_industry", ""),
"employee_count": data.get("employee_count"),
"employee_range": data.get("employee_range", ""),
"founded_year": data.get("founded_year"),
"headquarters": {
"city": data.get("city", ""),
"state": data.get("state", ""),
"country": data.get("country", ""),
},
"funding": {
"total_raised": data.get("total_funding"),
"last_round": data.get("last_funding_type", ""),
"last_round_date": data.get("last_funding_date"),
},
"tech_stack": data.get("technologies", []),
"social": {
"linkedin": data.get("linkedin_url", ""),
"twitter": data.get("twitter_url", ""),
},
"description": data.get("short_description", ""),
"logo_url": data.get("logo_url", ""),
}
# Usage
raw = lookup_company("notion.so")
if raw:
company = process_company_response(raw)
print(f"{company['name']} - {company['headquarters']['city']}")
print(f"Funding: ${company['funding']['total_raised']:,.0f}")
print(f"Tech: {', '.join(company['tech_stack'][:5])}")
Key principles when processing the response:
- Always use
.get()with defaults. Never assume a field exists. A 5-person startup may have no funding data. - Normalize data types. Employee count might come as a string or integer depending on the API. Parse it consistently.
- Store the raw response. Keep the original API response alongside your mapped version. If you add new fields to your model later, you can re-process without re-calling the API.
Step 5 — Cache Results
Company data doesn't change every minute. Stripe isn't going to switch industries between your two API calls today. Caching saves money, reduces latency, and protects you from rate limits.
The key insight is that different data types have different freshness requirements:
| Data Type | Recommended TTL | Reason |
|---|---|---|
| Firmographic (name, industry, HQ) | 7–30 days | Changes rarely |
| Employee count, funding | 24–72 hours | Can change with events |
| Tech stack | 7 days | Changes infrequently |
| Growth signals | 1–4 hours | Time-sensitive data |
| Social profiles | 30 days | Very stable |
Here's a Redis-based caching layer in Python:
import redis
import json
import hashlib
redis_client = redis.Redis(host="localhost", port=6379, db=0)
DEFAULT_TTL = 86400 # 24 hours in seconds
def get_cached_company(domain: str) -> dict | None:
"""Check cache for existing company data."""
cache_key = f"company:{domain}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
return None
def cache_company(domain: str, data: dict, ttl: int = DEFAULT_TTL):
"""Store company data in cache."""
cache_key = f"company:{domain}"
redis_client.setex(cache_key, ttl, json.dumps(data))
def lookup_company_cached(domain: str) -> dict | None:
"""Look up company with cache layer."""
# Check cache first
cached = get_cached_company(domain)
if cached:
return cached
# Cache miss — call API
data = lookup_company(domain)
if data:
cache_company(domain, data)
return data
For frontend applications, localStorage works for simple cases. For a discussion on caching strategies in web apps, Stack Overflow has extensive threads comparing approaches.
// Browser-side caching with localStorage
function getCachedCompany(domain) {
const key = `company:${domain}`;
const cached = localStorage.getItem(key);
if (cached) {
const { data, expiry } = JSON.parse(cached);
if (Date.now() < expiry) {
return data;
}
localStorage.removeItem(key); // Expired
}
return null;
}
function cacheCompany(domain, data, ttlMs = 86400000) {
const key = `company:${domain}`;
const entry = {
data,
expiry: Date.now() + ttlMs,
};
localStorage.setItem(key, JSON.stringify(entry));
}
Free Email Domain Detection
This is one of the most important edge cases in domain-based lookup, and one that many implementations get wrong. When a user signs up with [email protected], looking up gmail.com returns Google. That's technically correct but completely useless — John probably doesn't work at Google.
You need a list of free email providers to skip before calling the enrichment API. Here are the most common ones:
FREE_EMAIL_DOMAINS = {
# Major providers
"gmail.com", "googlemail.com",
"yahoo.com", "yahoo.co.uk", "yahoo.fr", "yahoo.de",
"hotmail.com", "hotmail.co.uk", "hotmail.fr",
"outlook.com", "outlook.co.uk",
"live.com", "live.co.uk",
"msn.com",
"aol.com",
"icloud.com", "me.com", "mac.com",
"protonmail.com", "proton.me",
"zoho.com",
"mail.com",
"gmx.com", "gmx.de", "gmx.net",
# Regional providers
"yandex.ru", "yandex.com",
"mail.ru",
"web.de",
"wp.pl",
"libero.it",
"laposte.net",
"orange.fr",
# Tech-oriented
"tutanota.com", "tuta.io",
"fastmail.com",
"hey.com",
"pm.me",
}
def is_free_email(domain: str) -> bool:
return domain.lower() in FREE_EMAIL_DOMAINS
In production, maintaining this list yourself is tedious. Open-source libraries exist for this purpose. The disposable-email-domains project on GitHub maintains a community-curated list with over 30,000 entries that includes both free and disposable email providers.
Some enrichment APIs, including the best ones in 2026, handle free email detection on their side and return a specific status code or flag. But it's still best practice to filter client-side to avoid wasting API calls.
Domain Lookup APIs Compared
Not all domain lookup APIs are created equal. Some focus on email finding, some on firmographic data, some on real-time signals. Here's how the major players compare for company data from domain lookups specifically.
| Feature | BounceWatch | Clearbit | Apollo | Hunter | Abstract API |
|---|---|---|---|---|---|
| Response Time | <500ms | <500ms | 1–3s | <1s | <1s |
| Company Data Fields | 40+ | 50+ | 30+ | 10+ | 15+ |
| Tech Stack Detection | Yes | Yes | Limited | No | No |
| Real-Time Signals | Yes (hiring, funding, product launches) | No (static data) | Limited | No | No |
| Startup Coverage | Strong (pre-seed to Series C) | Good (Series A+) | Good | Limited | Basic |
| Free Tier | 100 lookups/month | No free tier (since HubSpot acquisition) | 50 credits/month | 25 lookups/month | 100 lookups/month |
| Price per Lookup | From $0.01 | From $0.05 | Credit-based (~$0.03) | From $0.03 | From $0.01 |
| Batch Support | Yes (async) | Yes | Yes | Yes | No |
| Signal / Trigger Events | Yes — 40+ signal types | No | Job change alerts | No | No |
The biggest differentiator is whether the API returns static firmographic data or dynamic signals. Knowing a company has 200 employees is useful. Knowing they just posted 15 new engineering roles this week is actionable. If you're building for sales or investment workflows, signal data matters. Learn more about this in our Clearbit alternative comparison.
For a comprehensive breakdown of the enrichment API landscape, see our guide on the best company enrichment APIs in 2026.
Advanced: Batch Domain Lookup
Looking up one domain is straightforward. Looking up 10,000 is an engineering challenge. Whether you're enriching a CRM export or processing a lead list, batch domain lookup requires rate limiting, queuing, and async processing.
The Challenge
Most APIs enforce rate limits — typically 10–100 requests per second. If you fire 10,000 requests simultaneously, you'll get rate-limited after the first hundred. You also need to handle failures gracefully: network timeouts, temporary API errors, and domains that return no data.
The Solution: Queue-Based Processing
Here's a production-ready batch processor in Python using asyncio with rate limiting:
import asyncio
import aiohttp
import json
from datetime import datetime
API_KEY = "your_api_key_here"
BASE_URL = "https://api.bouncewatch.com/api/v1"
MAX_CONCURRENT = 10 # Max parallel requests
RATE_LIMIT_DELAY = 0.1 # Seconds between requests (10 req/sec)
async def lookup_single(session, domain, semaphore, results):
"""Look up a single domain with concurrency control."""
async with semaphore:
url = f"{BASE_URL}/company/{domain}"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json"
}
try:
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status == 200:
data = await resp.json()
results.append({"domain": domain, "status": "found", "data": data})
elif resp.status == 404:
results.append({"domain": domain, "status": "not_found", "data": None})
elif resp.status == 429:
# Rate limited — add back to queue with delay
await asyncio.sleep(2)
results.append({"domain": domain, "status": "rate_limited", "data": None})
else:
results.append({"domain": domain, "status": f"error_{resp.status}", "data": None})
except Exception as e:
results.append({"domain": domain, "status": "error", "data": None, "error": str(e)})
await asyncio.sleep(RATE_LIMIT_DELAY)
async def batch_lookup(domains: list[str]) -> list[dict]:
"""Process a batch of domains with rate limiting."""
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
results = []
async with aiohttp.ClientSession() as session:
tasks = [
lookup_single(session, domain, semaphore, results)
for domain in domains
]
await asyncio.gather(*tasks)
return results
# Usage
domains = ["stripe.com", "notion.so", "linear.app", "figma.com"]
results = asyncio.run(batch_lookup(domains))
found = [r for r in results if r["status"] == "found"]
print(f"Enriched {len(found)}/{len(domains)} domains")
for r in found:
print(f" {r['domain']}: {r['data']['name']} ({r['data'].get('employee_count', 'N/A')} employees)")
Scaling Beyond 10,000
For very large batches (100K+ domains), move to a job queue architecture:
- Upload the domain list to a CSV or database table
- A worker process picks domains from the queue, processes them, and stores results
- Progress tracking shows how many domains have been processed
- Retry logic re-queues failed lookups with exponential backoff
- Results export when the batch completes
If you're using Laravel, the built-in queue system with Redis handles this elegantly. For Node.js, libraries like BullMQ provide the same pattern. The key is separating the enqueueing (fast) from the processing (rate-limited).
With BounceWatch, you can also use the Signal Tracker for ongoing monitoring of your target companies — instead of doing one-time batch lookups, you track domains and receive signals automatically when something changes.
Cost Optimization Tips
- Deduplicate first. Your list of 10,000 emails might only contain 3,000 unique domains. Deduplicate before you start.
- Filter free emails. Remove gmail.com, yahoo.com, etc., before batch processing. This alone can cut your list by 30–50%.
- Check your cache. If you've enriched some of these domains before, skip them.
- Validate DNS. A quick DNS check (does this domain have an A record?) filters out dead domains without costing an API call.
For a complete guide on building this into a production pipeline, see our enrichment pipeline tutorial and our guide on automating CRM enrichment.
Putting It All Together
Domain-based company lookup is deceptively simple on the surface: domain in, company data out. But building a production-grade implementation requires careful attention to input parsing, free email filtering, error handling, caching, and batch processing. The difference between a prototype and a production system is in these details.
The good news is that the hard part — aggregating data from dozens of sources, running entity resolution, and keeping information fresh — is what the API handles for you. Your job is to feed it clean domains, cache the results, and use the data to build something your users love.
Here's a quick summary of the implementation checklist:
- Parse inputs — handle URLs, emails, and raw domains uniformly
- Filter free email domains — don't waste API calls on gmail.com
- Validate before calling — DNS check, deduplication, cache check
- Call the API with error handling — handle 404, 429, timeouts
- Process the response — map fields, handle nulls, store raw data
- Cache aggressively — different TTLs for different data types
- Batch intelligently — rate limiting, concurrency control, retry logic
Ready to try it? BounceWatch's domain-based company lookup API gives you structured company data, tech stack detection, and real-time growth signals from a single domain. Your first 100 lookups are free — no credit card required. Start building your enrichment pipeline today.