Domain-Based Company Lookup: How to Get Company Data from Just a URL

Api Data ·
Bounce Watch Bounce Watch Team
· · 20 min read · 335 views
Domain-Based Company Lookup: How to Get Company Data from Just a URL

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.com points 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.com has 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:

  1. User enters an email or URL (e.g., [email protected] or https://acme.com/about)
  2. You extract the domain (acme.com)
  3. You call a company data API with that domain
  4. The API returns structured JSON with 30+ data fields
  5. 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 clean
  • www.acme.com — strip the www.
  • https://acme.com/about/team — extract the hostname
  • [email protected] — extract the domain from the email
  • http://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:

  1. Upload the domain list to a CSV or database table
  2. A worker process picks domains from the queue, processes them, and stores results
  3. Progress tracking shows how many domains have been processed
  4. Retry logic re-queues failed lookups with exponential backoff
  5. 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:

  1. Parse inputs — handle URLs, emails, and raw domains uniformly
  2. Filter free email domains — don't waste API calls on gmail.com
  3. Validate before calling — DNS check, deduplication, cache check
  4. Call the API with error handling — handle 404, 429, timeouts
  5. Process the response — map fields, handle nulls, store raw data
  6. Cache aggressively — different TTLs for different data types
  7. 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.

Domain Lookup Company Data API Domain Enrichment Developer Tutorial URL To Company Data Enrichment
Share
Bounce Watch

Bounce Watch Team

Published on March 11, 2026

All Articles

Access startup data via our powerful API

Enrich your CRM, build integrations, and automate company research with the Bounce Watch API.

One analytics cookie (Google Analytics). Cookie Policy