Company URL Research API: Free Options and Working Code
Developer guide

Company URL Research API: Free Options and Working Code

Every practical way to turn a company name into its official website domain automatically. The free endpoint that needs no key, the paid options worth paying for, and copy ready code in JavaScript, Python and Google Sheets.

Updated: 5 August 2026 Endpoints re-tested: 5 August 2026 Level: beginner friendly
We do not sell an API. There is no paid plan on this site. The free tool on our homepage runs in your browser against the same public endpoint documented below, and this guide gives you everything you need to build the same thing yourself. Where a paid provider is genuinely the better answer, we say so and send you there.

1. What a company URL API actually is

You have a list of two hundred company names and no websites. Searching each one by hand takes hours. A company URL API does it in seconds: you send a name, it sends back a domain.

API stands for Application Programming Interface. Think of it as a counter you can walk up to. You hand over a request, someone in the back checks the records, and you get an answer. You never see the records, only the answer.

You send Salesforce. You get back salesforce.com. Put https:// in front and you have the full URL.

The answer arrives as JSON, a plain text format any programming language can read. This is a real response from the free endpoint, captured on 5 August 2026:

JSON, real response
[
  {
    "name": "Salesforce",
    "domain": "salesforce.com",
    "logo": null
  },
  {
    "name": "Salesforce Ben",
    "domain": "salesforceben.com",
    "logo": null
  }
]
Notice two things. First, logo is null. Older tutorials show a Clearbit CDN URL there, but the Logo API shut down on 8 December 2025 and that field has been empty ever since. Second, the second result is a completely different website that just shares a name prefix. That is the core problem this whole guide is really about.

2. Who uses one and why

WhoWhat they are doing
Sales and SDR teamsTurning a list of target account names into domains before building outreach sequences
Marketing operationsFilling in missing website fields on CRM records and ABM lists
Data analystsDeduplicating company datasets, where the domain is a far better key than the name
RecruitersGetting to official careers pages instead of third party job boards
DevelopersBuilding enrichment steps into an internal tool or integration

A common example: a team comes back from a trade show with three thousand scanned badges. Company names, no websites. Pasting that list through a lookup script gets them domains in minutes instead of days, and the domain is what every later step needs.

How many of those three thousand resolve correctly depends entirely on the list. A list of enterprise software brands behaves very differently from a list of regional contractors. Section 9 shows you how to measure that on your own data instead of trusting anyone’s headline number.

3. The free Clearbit Autocomplete endpoint

This is the simplest option available. No key, no account, no cost. It works from a browser, a server, or a spreadsheet.

Know what you are depending on. HubSpot acquired Clearbit in December 2023 and has been retiring the free surface ever since. Name to Domain was sunset on 30 April 2025. The Logo API closed on 8 December 2025. Autocomplete still responds, but it is undocumented, unsupported, has no published rate limit and no SLA. Fine for a script or an internal tool. Not something to build a paid product on without a fallback.
GET https://autocomplete.clearbit.com/v1/companies/suggest Free, no key
ParameterTypeRequiredNotes
querystringYesCompany name or partial name. URL encode it, or use your language’s encoder such as encodeURIComponent.

What comes back

FieldTypeReality in 2026
namestringPopulated
domainstringPopulated. This is the useful one.
logonullAlways null since December 2025

That is the entire response. There is no industry, headcount, revenue, location or founding year. If you see a tool showing those fields next to an Autocomplete lookup, it is either paying a second provider or inventing them.

Results come back sorted by relevance, so index [0] is usually right when the input name is specific. For short or common names it often is not, which is what the next section handles.

4. JavaScript: production ready lookup

Most tutorials stop at “take the first result”. That is exactly how wrong domains end up in a CRM. This version scores how closely the returned name matches what you asked for, and hands back the alternatives so you can catch the misses.

JavaScript, browser and Node 18+
// Company name to domain lookup with match scoring.
// Free, no API key. Works in the browser and in Node 18 or later.

const ENDPOINT = 'https://autocomplete.clearbit.com/v1/companies/suggest?query=';

// Legal suffixes hurt matching. "HubSpot Inc." finds far worse than "HubSpot".
const SUFFIXES = /\b(inc|incorporated|corp|corporation|co|company|ltd|limited|llc|llp|plc|gmbh|ag|sa|srl|spa|bv|nv|ab|oy|pty|pvt|private|group|holdings|international|technologies|solutions|services)\b\.?/gi;

function normalise(s) {
  return String(s || '').toLowerCase()
    .replace(/[^a-z0-9 ]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function stripSuffix(name) {
  const out = String(name).replace(SUFFIXES, ' ').replace(/\s+/g, ' ').trim();
  return out.length >= 2 ? out : String(name).trim();
}

// Dice coefficient over character bigrams. Returns 0 to 1.
function similarity(a, b) {
  a = normalise(a); b = normalise(b);
  if (!a || !b) return 0;
  if (a === b) return 1;
  if (a.length < 2 || b.length < 2) return 0;

  const pairs = new Map();
  for (let i = 0; i < a.length - 1; i++) {
    const bg = a.slice(i, i + 2);
    pairs.set(bg, (pairs.get(bg) || 0) + 1);
  }
  let hits = 0;
  for (let i = 0; i < b.length - 1; i++) {
    const bg = b.slice(i, i + 2);
    if (pairs.get(bg) > 0) { pairs.set(bg, pairs.get(bg) - 1); hits++; }
  }
  const dice = (2 * hits) / (a.length - 1 + b.length - 1);

  // "Zoom" vs "Zoom Video Communications" scores near zero on raw bigrams
  // but is probably right. Lift whole-word prefixes into the middle band.
  // Deliberately below the High cutoff: "Salesforce" is also a prefix of
  // "Salesforce Ben", which is a different website entirely.
  const [short, long] = a.length <= b.length ? [a, b] : [b, a];
  if (long.startsWith(short + ' ')) {
    return Math.max(dice, short.length >= 6 ? 0.78 : 0.60);
  }
  return dice;
}

function bandFor(score) {
  if (score >= 0.85) return 'High';
  if (score >= 0.50) return 'Medium';
  return 'Low';
}

async function findCompanyURL(companyName) {
  const query = stripSuffix(companyName);

  let res;
  try {
    res = await fetch(ENDPOINT + encodeURIComponent(query));
  } catch (err) {
    // Network failure is NOT the same as "company not found".
    // Conflating them is the most common bug in these scripts.
    return { input: companyName, status: 'request_failed', error: String(err) };
  }
  if (!res.ok) {
    return { input: companyName, status: 'request_failed', error: 'HTTP ' + res.status };
  }

  const data = await res.json();
  if (!Array.isArray(data) || data.length === 0) {
    return { input: companyName, status: 'not_found' };
  }

  const ranked = data
    .map(c => ({
      name: c.name,
      domain: c.domain,
      score: Math.max(similarity(query, c.name), similarity(companyName, c.name))
    }))
    .sort((a, b) => b.score - a.score);

  const best = ranked[0];
  return {
    input:        companyName,
    status:       'ok',
    matchedName:  best.name,
    domain:       best.domain,
    url:          'https://' + best.domain,
    score:        Number(best.score.toFixed(2)),
    band:         bandFor(best.score),
    alternatives: ranked.slice(1, 4)   // check these when band is not High
  };
}

// Bulk, with a small delay so you are polite to a free endpoint.
const sleep = ms => new Promise(r => setTimeout(r, ms));

async function bulkLookup(names, delayMs = 250) {
  const out = [];
  for (const name of names) {
    out.push(await findCompanyURL(name));
    await sleep(delayMs);
  }
  return out;
}

// Example
bulkLookup(['Salesforce', 'HubSpot Inc.', 'Bolt', 'Notion', 'Figma'])
  .then(rows => {
    console.table(rows.map(r => ({
      input: r.input, domain: r.domain || '', band: r.band || r.status
    })));
    const review = rows.filter(r => r.band === 'Medium' || r.band === 'Low');
    console.log('Rows needing a human check:', review.length);
  });
The distinction that matters most. The code returns three different statuses: ok, not_found and request_failed. Most example scripts collapse the last two into “not found”. Then the endpoint has an outage, every row comes back empty, and someone assumes the whole list is bad data. Keep them separate.

Calling it from a browser

The endpoint sends permissive CORS headers, so browser calls work. Two things will still break it: an ad blocker or privacy extension may block the request, and a corporate network may filter the domain. Handle the failure path visibly rather than silently.

5. Python: bulk lookup to CSV

The same logic in Python, writing straight to a CSV you can open in Excel or Sheets. Install the one dependency with pip install requests.

Python 3.8+
"""Company name to domain lookup with match scoring.
Free Clearbit Autocomplete endpoint. No API key needed.
"""

import csv
import re
import time
import requests

ENDPOINT = "https://autocomplete.clearbit.com/v1/companies/suggest"

SUFFIXES = re.compile(
    r"\b(inc|incorporated|corp|corporation|co|company|ltd|limited|llc|llp|plc|"
    r"gmbh|ag|sa|srl|spa|bv|nv|ab|oy|pty|pvt|private|group|holdings|"
    r"international|technologies|solutions|services)\b\.?",
    re.IGNORECASE,
)


def normalise(s: str) -> str:
    s = re.sub(r"[^a-z0-9 ]+", " ", str(s or "").lower())
    return re.sub(r"\s+", " ", s).strip()


def strip_suffix(name: str) -> str:
    out = re.sub(r"\s+", " ", SUFFIXES.sub(" ", str(name))).strip()
    return out if len(out) >= 2 else str(name).strip()


def similarity(a: str, b: str) -> float:
    """Dice coefficient over character bigrams, 0 to 1."""
    a, b = normalise(a), normalise(b)
    if not a or not b:
        return 0.0
    if a == b:
        return 1.0
    if len(a) < 2 or len(b) < 2:
        return 0.0

    pairs = {}
    for i in range(len(a) - 1):
        bg = a[i:i + 2]
        pairs[bg] = pairs.get(bg, 0) + 1

    hits = 0
    for i in range(len(b) - 1):
        bg = b[i:i + 2]
        if pairs.get(bg, 0) > 0:
            pairs[bg] -= 1
            hits += 1

    dice = (2 * hits) / (len(a) - 1 + len(b) - 1)

    short, long = (a, b) if len(a) <= len(b) else (b, a)
    if long.startswith(short + " "):
        return max(dice, 0.78 if len(short) >= 6 else 0.60)
    return dice


def band_for(score: float) -> str:
    if score >= 0.85:
        return "High"
    if score >= 0.50:
        return "Medium"
    return "Low"


def find_company_url(company_name: str) -> dict:
    query = strip_suffix(company_name)
    try:
        resp = requests.get(ENDPOINT, params={"query": query}, timeout=10)
        resp.raise_for_status()
        data = resp.json()
    except Exception as exc:
        # A failed request is not the same as "no such company".
        return {"input": company_name, "status": "request_failed", "error": str(exc)}

    if not data:
        return {"input": company_name, "status": "not_found"}

    ranked = sorted(
        (
            {
                "name": c.get("name", ""),
                "domain": c.get("domain", ""),
                "score": max(
                    similarity(query, c.get("name", "")),
                    similarity(company_name, c.get("name", "")),
                ),
            }
            for c in data
        ),
        key=lambda r: r["score"],
        reverse=True,
    )

    best = ranked[0]
    return {
        "input": company_name,
        "status": "ok",
        "matched_name": best["name"],
        "domain": best["domain"],
        "url": "https://" + best["domain"],
        "score": round(best["score"], 2),
        "band": band_for(best["score"]),
        "alternatives": "; ".join(f"{r['name']} ({r['domain']})" for r in ranked[1:4]),
    }


def bulk_lookup(names, delay=0.25):
    results = []
    total = len(names)
    for i, name in enumerate(names, 1):
        print(f"[{i}/{total}] {name}")
        results.append(find_company_url(name))
        time.sleep(delay)
    return results


def save_csv(rows, filename="company_urls.csv"):
    cols = ["input", "status", "matched_name", "domain", "url",
            "score", "band", "alternatives"]
    with open(filename, "w", newline="", encoding="utf-8-sig") as fh:
        writer = csv.DictWriter(fh, fieldnames=cols, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)
    print(f"Wrote {len(rows)} rows to {filename}")


if __name__ == "__main__":
    companies = ["Salesforce", "HubSpot Inc.", "Stripe", "Shopify", "Bolt", "Notion"]
    rows = bulk_lookup(companies)
    save_csv(rows)

    ok = sum(1 for r in rows if r["status"] == "ok")
    high = sum(1 for r in rows if r.get("band") == "High")
    failed = sum(1 for r in rows if r["status"] == "request_failed")
    print(f"Resolved {ok}/{len(rows)}. High confidence: {high}. Request errors: {failed}.")

utf-8-sig in the CSV writer is deliberate. It adds a byte order mark so Excel renders accented company names correctly instead of turning them into mojibake.

6. Google Sheets with no coding

Not a developer? This gives you a formula you can drag down a column. Setup takes about five minutes.

Put your names in column A

Header in A1, first company in A2.

Open Extensions, then Apps Script

A code editor opens in a new tab.

Delete what is there and paste the code below

Then click the save icon.

Type the formula in B2

Enter =GETCOMPANYURL(A2) and drag it down the column.

Google Apps Script
/**
 * Look up a company's official website domain.
 * Free Clearbit Autocomplete endpoint, no API key.
 *
 * @param {string} companyName Company name to look up.
 * @param {number} refresh     Optional. Pass a changing cell to force a re-run.
 * @return {string} The URL, or a readable status.
 * @customfunction
 */
function GETCOMPANYURL(companyName, refresh) {
  if (!companyName || String(companyName).trim() === '') return '';

  var name = String(companyName).trim()
    .replace(/\b(Inc|Corp|Corporation|Co|Ltd|Limited|LLC|PLC|GmbH|Pvt|Private)\b\.?/gi, '')
    .replace(/\s+/g, ' ')
    .trim();

  var url = 'https://autocomplete.clearbit.com/v1/companies/suggest?query='
          + encodeURIComponent(name);

  try {
    var res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    if (res.getResponseCode() !== 200) return 'Lookup unavailable';

    var data = JSON.parse(res.getContentText());
    if (!data || data.length === 0) return 'Not found';

    return 'https://' + data[0].domain;
  } catch (e) {
    return 'Lookup unavailable';
  }
}

/**
 * Returns the name the database actually matched, so you can eyeball
 * whether the domain belongs to the company you meant.
 * @customfunction
 */
function GETMATCHEDNAME(companyName, refresh) {
  if (!companyName || String(companyName).trim() === '') return '';
  var url = 'https://autocomplete.clearbit.com/v1/companies/suggest?query='
          + encodeURIComponent(String(companyName).trim());
  try {
    var res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    if (res.getResponseCode() !== 200) return 'Lookup unavailable';
    var data = JSON.parse(res.getContentText());
    return (data && data.length) ? data[0].name : 'Not found';
  } catch (e) {
    return 'Lookup unavailable';
  }
}

Put =GETCOMPANYURL(A2) in column B and =GETMATCHEDNAME(A2) in column C. Reading those two side by side is the fastest way to spot a wrong match, because “Bolt” returning “Bolt Financial” is instantly obvious in a way that a bare domain is not.

Two limits to expect. Apps Script caches custom function results, so editing a name may show the old answer. Add a helper column with =RAND() and pass it as the second argument to force a re-run. And Sheets caps how long custom functions may run, so process a few hundred rows at a time rather than ten thousand at once.

7. Paid options: Hunter.io and Apollo

Reach for these when you need more than a domain, or when you need something with support behind it.

Hunter.io Company Enrichment

GET https://api.hunter.io/v2/companies/find

Takes a domain or a company name and returns real firmographics: description, industry, country, city, founded year and headcount. This is the closest supported replacement for the retired Clearbit enrichment product.

PlanMonthlyAnnualCredits per month
Free$0$050
Starter$49$342,000
Growth$149$10410,000
Scale$299$20925,000
EnterpriseCustomCustom

Annual billing is a flat 30 percent discount. Credits are a shared pool across finding and verification. Pricing checked against hunter.io/pricing in August 2026, but check it yourself before budgeting, because vendor pricing moves.

Python, Hunter.io
import os
import requests

API_KEY = os.environ["HUNTER_API_KEY"]   # never hard-code a key in your source

def enrich_company(name_or_domain: str, by_domain: bool = False) -> dict:
    params = {"api_key": API_KEY}
    params["domain" if by_domain else "company"] = name_or_domain

    resp = requests.get("https://api.hunter.io/v2/companies/find",
                        params=params, timeout=15)

    if resp.status_code == 401:
        raise RuntimeError("Bad API key")
    if resp.status_code == 429:
        raise RuntimeError("Rate limited or out of credits")
    resp.raise_for_status()

    return resp.json().get("data") or {}

info = enrich_company("Shopify")
print(info.get("domain"), info.get("industry"), info.get("country"))

Apollo.io

A full sales platform rather than a lookup endpoint. Worth it if you also need contacts, sequences and intent signals in the same place. Overkill if all you want is a domain.

Never put an API key in front end code. Anything in browser JavaScript is readable by anyone who opens developer tools, including your key. Call paid APIs from a server, a serverless function or a backend job. The free Clearbit endpoint is the exception only because there is no key to leak.

8. Side by side comparison

OptionCostKey neededBulkFirmographicsSupported
Clearbit AutocompleteFreeNoYour own loopNoNo, undocumented
Our free toolFreeNo25 per run, in browserNoBest effort
Google Sheets scriptFreeNoSlow, per cellNoNo
Hunter.io free$0, 50 creditsYesNoYesYes
Hunter.io Starter$34 to $49 per monthYesYesYesYes
Apollo.ioPaidYesYesYesYes

Which to pick

  • Prototyping or a one off list: the free Clearbit endpoint. Running in two minutes, nothing to sign up for.
  • Not a developer: the Google Sheets function above, or our free tool if you would rather paste and click.
  • You need industry, headcount or location: Hunter.io. The free endpoint simply does not carry that data.
  • Shipping this inside a paid product: a supported provider, or the free endpoint plus a cache and a documented fallback. Do not put an undocumented endpoint on your critical path alone.

9. Measure your own accuracy

Every article on this topic quotes accuracy percentages. Almost none of them say what list produced those numbers, which makes them close to meaningless. Accuracy depends on your data, not on ours.

So here is a script instead of a statistic. Take fifty companies from your real list, look up the correct domain by hand once, and run this. Ten minutes of work gives you a number that is actually about you.

Python, accuracy benchmark
"""Measure lookup accuracy on YOUR list.

1. Build a CSV called truth.csv with two columns: name,correct_domain
2. Fill in 30 to 50 rows from your real data, verified by hand.
3. Run this. Requires find_company_url() from the section above.
"""

import csv
from collections import Counter


def load_truth(path="truth.csv"):
    with open(path, newline="", encoding="utf-8-sig") as fh:
        return list(csv.DictReader(fh))


def clean_domain(d):
    return (d or "").lower().replace("www.", "").strip().rstrip("/")


def benchmark(path="truth.csv"):
    rows = load_truth(path)
    bands = Counter()
    correct_by_band = Counter()
    misses = []

    for row in rows:
        expected = clean_domain(row["correct_domain"])
        result = find_company_url(row["name"])

        if result["status"] != "ok":
            bands[result["status"]] += 1
            misses.append((row["name"], expected, result["status"]))
            continue

        band = result["band"]
        bands[band] += 1
        got = clean_domain(result["domain"])

        if got == expected:
            correct_by_band[band] += 1
        else:
            misses.append((row["name"], expected, got + " [" + band + "]"))

    total = len(rows)
    right = sum(correct_by_band.values())
    print(f"Overall: {right}/{total} = {right / total:.0%}\n")

    for band in ("High", "Medium", "Low"):
        n = bands[band]
        if n:
            print(f"{band:7} {correct_by_band[band]:3}/{n:3} "
                  f"= {correct_by_band[band] / n:.0%}")

    if bands["not_found"]:
        print(f"\nNot in database: {bands['not_found']}")
    if bands["request_failed"]:
        print(f"Request errors:  {bands['request_failed']} (re-run these)")

    print("\nMisses:")
    for name, expected, got in misses[:25]:
        print(f"  {name:30} expected {expected:28} got {got}")


if __name__ == "__main__":
    benchmark()

The per band breakdown is the part that pays off. If High rows come back right almost every time on your data, you can auto import them and only review Medium and Low. If High is unreliable for you, that tells you something important about your list before you send a single email.

What tends to go wrong, and why

Kind of nameTypical problemWhat to do
Large global brandsRarely a problemTrust High rows, spot check a sample
Short or shared namesBolt, Mercury, Atlas, Apex and Dove all belong to several real companiesAlways read the alternatives list
Full legal namesThe database stores the trading nameStrip the legal suffix first, as the code above does
SubsidiariesResolves to the parent insteadSearch the brand, not the corporate group
Small local businessesOften not in the database at allA manual search is faster than fighting it
Recently rebrandedStale domain. Notion moved from notion.so to notion.com in June 2026Re-run lists older than a few months

10. Five mistakes that waste credits and time

  1. Treating a network error as “not found”. One outage and your whole list looks like bad data. Keep the two states apart.
  2. Taking result [0] without scoring it. This is how a competitor’s domain ends up in your outreach sequence.
  3. Firing every request at once. A free undocumented endpoint will start refusing you. Cap concurrency and add a small delay.
  4. Not caching. Company domains barely change. Cache by normalised name for thirty days and most repeat lookups disappear.
  5. Sending legal names. “Acme Technologies Private Limited” matches badly. “Acme” matches well. Strip suffixes before you send.
A sensible production shape. Put the lookup behind your own small server endpoint rather than calling it from the browser. Cache results by normalised name. Log which names fail so you can fix your input data. Keep a second provider configured behind a flag, so a sunset announcement is a config change rather than an incident.

Frequently asked questions

Is there a free company URL lookup API with no sign-up?
Yes. The Clearbit Autocomplete endpoint needs no key and no account. Send a company name as the query parameter and it returns matches with their primary domain. It is undocumented and unsupported, so treat it as best effort rather than production infrastructure.
What happened to the Clearbit Name to Domain and Logo APIs?
HubSpot acquired Clearbit in December 2023. Name to Domain at company.clearbit.com/v2 was sunset on 30 April 2025. The Logo API shut down on 8 December 2025, which is why the logo field in Autocomplete responses is now always null. If a tutorial shows you a logo URL there, it was written before that date.
Does Autocomplete return industry, headcount or location?
No. Three fields only: name, domain and logo, and logo is null. Nothing else. Any tool displaying industry next to an Autocomplete result is either paying a second provider or fabricating it.
Do you sell a company URL research API?
No. There is no paid plan on this site and no API to buy. The free tool on our homepage runs entirely in your browser against the same public endpoint documented here. If you need something supported with an SLA, use Hunter.io or Apollo. An earlier version of this page described an API we do not offer, and we have removed it.
How accurate is a name to domain lookup?
It depends entirely on your list, which is why we publish a benchmark script instead of a headline percentage. Global brands resolve reliably. Short shared names such as Bolt, Mercury and Atlas are frequently wrong. Very small local businesses often are not in the database. Run the script in section 9 against thirty of your own companies and you will have a number that actually applies to you.
Can I use this in Google Sheets without coding?
Yes. Open Extensions then Apps Script, paste the function from section 6, save, and type =GETCOMPANYURL(A2) in a cell. About five minutes of setup and no coding beyond copy and paste.
What is the difference between name to domain and domain to company?
Opposite directions. Name to domain takes “Salesforce” and gives you salesforce.com, which is what this guide and our tool cover. Domain to company takes salesforce.com and gives you industry, headcount, description and location. Hunter.io does both. The free Clearbit endpoint does only the first.
How do I find a company’s careers page programmatically?
Get the domain first, then try the common patterns in order: /careers, /jobs, careers.domain.com and jobs.domain.com. Most companies use one of those four. Our Careers Finder tab builds the same list for you. Be clear that this only constructs likely URLs. It does not verify that the page exists and it carries no job listing data.
Is this site connected to Target Corporation?
No. A target company is a standard business term meaning any company you are researching or prospecting. Target Corporation is the American retailer, at target.com and corporate.target.com. This is an independent free tool for B2B research with no connection to them.

Do not want to write any code?

Paste up to 25 company names into the free tool and get the same results with match scores and a CSV export. No sign-up, no card.

Last updated 5 August 2026 by Rizwan Aslam. All endpoints in this guide were re-tested on that date. Third party pricing changes without notice, so confirm it on the vendor’s own page before you commit budget. Spotted something out of date? Tell us and we will correct it.