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.
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:
[
{
"name": "Salesforce",
"domain": "salesforce.com",
"logo": null
},
{
"name": "Salesforce Ben",
"domain": "salesforceben.com",
"logo": null
}
]
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
| Who | What they are doing |
|---|---|
| Sales and SDR teams | Turning a list of target account names into domains before building outreach sequences |
| Marketing operations | Filling in missing website fields on CRM records and ABM lists |
| Data analysts | Deduplicating company datasets, where the domain is a far better key than the name |
| Recruiters | Getting to official careers pages instead of third party job boards |
| Developers | Building 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.
| Parameter | Type | Required | Notes |
|---|---|---|---|
query | string | Yes | Company name or partial name. URL encode it, or use your language’s encoder such as encodeURIComponent. |
What comes back
| Field | Type | Reality in 2026 |
|---|---|---|
name | string | Populated |
domain | string | Populated. This is the useful one. |
logo | null | Always 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.
// 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);
});
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.
"""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.
/**
* 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.
=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
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.
| Plan | Monthly | Annual | Credits per month |
|---|---|---|---|
| Free | $0 | $0 | 50 |
| Starter | $49 | $34 | 2,000 |
| Growth | $149 | $104 | 10,000 |
| Scale | $299 | $209 | 25,000 |
| Enterprise | Custom | Custom | |
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.
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.
8. Side by side comparison
| Option | Cost | Key needed | Bulk | Firmographics | Supported |
|---|---|---|---|---|---|
| Clearbit Autocomplete | Free | No | Your own loop | No | No, undocumented |
| Our free tool | Free | No | 25 per run, in browser | No | Best effort |
| Google Sheets script | Free | No | Slow, per cell | No | No |
| Hunter.io free | $0, 50 credits | Yes | No | Yes | Yes |
| Hunter.io Starter | $34 to $49 per month | Yes | Yes | Yes | Yes |
| Apollo.io | Paid | Yes | Yes | Yes | Yes |
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.
"""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 name | Typical problem | What to do |
|---|---|---|
| Large global brands | Rarely a problem | Trust High rows, spot check a sample |
| Short or shared names | Bolt, Mercury, Atlas, Apex and Dove all belong to several real companies | Always read the alternatives list |
| Full legal names | The database stores the trading name | Strip the legal suffix first, as the code above does |
| Subsidiaries | Resolves to the parent instead | Search the brand, not the corporate group |
| Small local businesses | Often not in the database at all | A manual search is faster than fighting it |
| Recently rebranded | Stale domain. Notion moved from notion.so to notion.com in June 2026 | Re-run lists older than a few months |
10. Five mistakes that waste credits and time
- Treating a network error as “not found”. One outage and your whole list looks like bad data. Keep the two states apart.
- Taking result
[0]without scoring it. This is how a competitor’s domain ends up in your outreach sequence. - Firing every request at once. A free undocumented endpoint will start refusing you. Cap concurrency and add a small delay.
- Not caching. Company domains barely change. Cache by normalised name for thirty days and most repeat lookups disappear.
- Sending legal names. “Acme Technologies Private Limited” matches badly. “Acme” matches well. Strip suffixes before you send.
Frequently asked questions
Is there a free company URL lookup API with no sign-up?
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?
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?
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?
How accurate is a name to domain lookup?
Can I use this in Google Sheets without coding?
=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?
How do I find a company’s careers page programmatically?
/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?
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.