Use cases / Website Contacts API
Lead enrichment from company websites
Who
Who runs this, and what for
| Team | What they decide with it |
|---|---|
| Sales and RevOps | Which accounts have a reachable inbox or phone before they are assigned to a rep. |
| Marketplaces and directories | Whether a supplier's listed contact details still match what their website says. |
| Procurement and vendor research | Who to contact at a vendor, and where the company is registered. |
| Data teams | How to fill empty company fields in the CRM without buying a contact database. |
Pipeline
The calls, step by step
01Start from domains
The input is a public hostname, with or without https://. Domains usually come from the CRM, a sign-up form, or another API: the Google Maps API returns each local business's website, and the Product Hunt API returns each launch's website. IP addresses and localhost are rejected before any fetch.
GET /v1/contacts?domain=stripe.com · pages
[ { "url": "https://stripe.com/", "type": "home" }, { "url": "https://stripe.com/about", "type": "about" }, { "url": "https://stripe.com/privacy", "type": "privacy" } ]Recorded response. Live values differ. 02Look up what the company published
One call reads the homepage and up to seven linked pages on the same site, choosing contact, about, imprint, team, support, and privacy pages first (pages sets the total; default 5, max 8). The price is one request whatever pages you pass. It returns company details from the site's metadata, then every email, phone, social profile, address, and named person it found, each with sources: the URLs it came from.
GET /v1/contacts?domain=stripe.com · company
{ "name": "Stripe", "legal_name": "Stripe, LLC", "description": "Stripe powers online and in-person payment processing and financial solutions for businesses of all sizes.", "logo": "https://images.stripeassets.com/fzn2n1nzq965/1hgcBNd12BfT9VLgbId7By/01d91920114b124fb4cf6d448f9f06eb/favicon.svg", "country": "US", "language": "en-US" }Recorded response. Live values differ. 03Pick the fields worth keeping
emails[] marks each address generic (a role inbox such as sales@) or personal, and domain_match says whether it is on the queried domain; match_email_domain=true drops the rest. Phones come normalized to E.164 with the country. people[] lists names the site published, often without a job title or email, so treat it as a lead to research rather than a contact.
GET /v1/contacts?domain=stripe.com · emails
[ { "value": "privacy@stripe.com", "type": "generic", "domain_match": true, "sources": [ "https://stripe.com/privacy" ] } ]Recorded response. Live values differ.
Code
A script to start from
enrich_domains.py
# Enrich a list of company domains with the contacts they publish.
# In: domains.txt, one domain per line. Out: contacts.csv, one row per domain.
import csv, os, requests
API = "https://api.clair.im"
HEADERS = {"Authorization": f"Bearer {os.environ['CLAIR_API_KEY']}"}
def lookup(domain):
r = requests.get(f"{API}/v1/contacts", headers=HEADERS, timeout=90,
params={"domain": domain, "pages": 5})
r.raise_for_status()
return r.json()
def best_email(emails):
# An address on the company's own domain first, role inboxes before people.
ranked = sorted(emails, key=lambda e: (not e["domain_match"], e["type"] != "generic"))
return ranked[0]["value"] if ranked else None
with open("domains.txt") as f:
domains = [line.strip() for line in f if line.strip()]
with open("contacts.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["domain", "company", "email", "email_source", "phone",
"linkedin", "country"])
for domain in domains:
try:
c = lookup(domain)
except requests.HTTPError as e:
print(domain, e.response.status_code, e.response.text[:200])
continue
email = best_email(c["emails"])
source = next((e["sources"][0] for e in c["emails"] if e["value"] == email), None)
linkedin = next((s["url"] for s in c["social"] if s["network"] == "linkedin"), None)
company = c["company"] or {}
out.writerow([domain, company.get("name"), email, source,
c["phones"][0]["e164"] if c["phones"] else None,
linkedin, company.get("country")])
Set CLAIR_API_KEY to a key subscribed to the Website Contacts API. Each call is one request against that API's monthly quota.
Cost
What it costs per month
| Schedule | Requests | Plan | Per month | Per 1,000 |
|---|---|---|---|---|
| 1,000 domains, one-offOne lookup per domain. | 1,000 | Pay as you go | $20.10 | $20.10 |
| 5,000 new domains a monthNew sign-ups or leads enriched as they arrive. | 5,000 | Pro | $67.41 | $13.48 |
| 20,000 domains, refreshed quarterly20,000 ÷ 3 months, rounded up, spread evenly. | 6,700 | Pro | $90.19 | $13.46 |
| 50,000 new domains a monthA large lead list enriched in one month. | 50,000 | Mega | $402.24 | $8.04 |
Limits
What to plan for
- Only what the company published. There is no inference of email patterns (first.last@), no third-party database, and no verification that an inbox accepts mail. Run a deliverability check before sending.
- The lookup follows links from the homepage. It does not walk the sitemap or log in, so a contact page not linked from the homepage can be missed.
- Personal data rules still apply. An address a company published is not consent to email the person behind it; check the rules where you and the recipient are, such as GDPR in the EU and UK.
- Many companies publish only a form, not an email. An empty emails[] is a real answer, and the phone or LinkedIn profile is often the better contact.
- Use results in line with each site's terms, and do not resell them as a contact directory.
FAQ
Common questions
Why does it return info@ and not the CEO's email?
Because that is what most companies publish. Clair returns addresses found on the site, marked generic or personal, and does not guess addresses that do not appear on any page.
Does a larger pages value cost more?
No. A lookup is one request whether it reads one page or eight. More pages find more contacts but take longer.
How fresh is the data?
It is read when you call. Nothing is served from a stored database, so a changed phone number shows up on the next lookup.
Can I look up a person instead of a domain?
No. The input is a company domain. Named people appear only when the company lists them on its own pages.
Run it on your own products
200 requests a month free on each API, no card. Enough to run the pipeline on a short list before choosing a plan.