We have a spreadsheet of company names with no domains attached. Is there a tool that can resolve them into real website domains in bulk instead of googling each one by hand?
Resolve Company Names to Domains with Clay
Clean a CSV of company names into website domains — the key that unlocks every other Clay enrichment function — using Clay's Company Domain routine.
What you will build
A Flask service exposing /resolve-domains, which reads a CSV of company names, submits them to Clay's real domain-resolution routine in chunks, and writes back a CSV with resolved domains.
POST /resolve-domains (CSV: company_name)
↓
POST /routines/{routine_id}/run (Company Domain, chunked at 100/request)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
resolved_domains.csv (company_name, domain)
AI Prompt
Implement a Flask service that resolves a CSV of company names to domains
using Clay's "Company Domain" managed function.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Find the real routine id via your workspace's routine catalog.
- Confirmed real input schema: {"Company Name": str} (required).
- Confirmed real result key: "Domain" (a plain hostname string).
- IMPORTANT LIMITATION (confirmed live): this routine does not signal "not
found" for unmatchable/nonsense company names -- it can return a
confidently wrong domain instead of an empty result. Disclose this in the
article; do not attempt to silently filter or validate results as if the
routine reliably distinguishes real matches from guesses.
- Accept a CSV via multipart upload or raw request body, with a
"company_name" column.
- Chunk submissions at up to 100 items per API call.
- Write resolved_domains.csv with columns company_name,domain -- empty
string for anything genuinely unresolved (a missing result), never a
fabricated domain from this code's own logic.
- Run the verification step below before finishing.
Prerequisites
- Python 3.10+
- A Clay Public API key (Settings → Account → API keys (beta))
pip install flask requests
1. Create the project
mkdir company-name-to-domain && cd company-name-to-domain python -m venv venv && source venv/bin/activate pip install flask requests
2. Discover the routine and its real schema
clay routines list | grep -A2 '"Company Domain"'
clay routines get function:t_XXXXXXXXXXXXXXXXXXXX
3. Configure credentials
CLAY_API_KEY= CLAY_ROUTINE_ID_DOMAIN=function:t_XXXXXXXXXXXXXXXXXXXX
In a CI/sandbox test environment, CLAY_API_KEY and CLAY_ROUTINE_ID_DOMAIN are typically provided as pre-configured secrets; in your own deployment, set them as real environment variables or via your platform's secrets manager.
4. Implement the domain-resolution service
"""
Bulk company-name -> domain resolver, backed by the Clay 'Company Domain'
routine.
POST /resolve-domains with a CSV containing a 'company_name' column, either
as a multipart file upload or as the raw request body. Writes
resolved_domains.csv with columns company_name,domain. A missing result
gets an empty domain -- never a guessed one from this code.
KNOWN LIMITATION (confirmed live, see Common Issues): the routine itself
can return a confidently wrong domain for a nonsense/unmatchable company
name instead of signaling "not found". This is a property of Clay's
underlying data provider, not a defect in this code.
"""
import csv
import io
import os
import time
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
BASE_URL = "https://api.clay.com/public/v0"
HEADERS = {"clay-api-key": os.environ["CLAY_API_KEY"], "Content-Type": "application/json"}
ROUTINE_ID = os.environ["CLAY_ROUTINE_ID_DOMAIN"]
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", 5))
MAX_WAIT = int(os.environ.get("MAX_WAIT_SECONDS", 300))
CHUNK_SIZE = 100
def read_company_names(raw_body, content_type):
if content_type and "multipart" in content_type and request.files:
file = next(iter(request.files.values()))
text = file.read().decode("utf-8")
else:
text = raw_body.decode("utf-8")
reader = csv.DictReader(io.StringIO(text))
return [row["company_name"] for row in reader if row.get("company_name")]
def submit_chunk(names, offset):
items = [{"id": f"row-{offset + i}", "inputs": {"Company Name": name}} for i, name in enumerate(names)]
resp = requests.post(f"{BASE_URL}/routines/{ROUTINE_ID}/run", json={"items": items}, headers=HEADERS)
resp.raise_for_status()
return resp.json()["routine_run_id"]
def poll(run_id):
deadline = time.time() + MAX_WAIT
while time.time() < deadline:
resp = requests.get(f"{BASE_URL}/routines/run/{run_id}/results", headers=HEADERS)
resp.raise_for_status()
body = resp.json()
if body["status"] == "complete":
return body.get("data", [])
time.sleep(POLL_INTERVAL)
raise TimeoutError(f"Routine run {run_id} did not complete within {MAX_WAIT}s")
@app.route("/resolve-domains", methods=["POST"])
def resolve_domains():
names = read_company_names(request.get_data(), request.content_type)
rows, run_ids = [], []
for offset in range(0, len(names), CHUNK_SIZE):
chunk = names[offset:offset + CHUNK_SIZE]
run_id = submit_chunk(chunk, offset)
run_ids.append(run_id)
items = poll(run_id)
for i, item in enumerate(items):
domain = (item.get("result") or {}).get("Domain", "")
rows.append({"company_name": chunk[i], "domain": domain or ""})
with open("resolved_domains.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["company_name", "domain"])
writer.writeheader()
writer.writerows(rows)
unresolved = sum(1 for r in rows if not r["domain"])
return jsonify({
"output_file": "resolved_domains.csv",
"total": len(rows),
"resolved": len(rows) - unresolved,
"unresolved": unresolved,
"routine_run_ids": run_ids,
"rows": rows,
})
if __name__ == "__main__":
app.run(port=5015)
5. Run the application
python app.py
printf "company_name\nAnthropic\nClay\nStripe\n" | curl -s -X POST http://localhost:5015/resolve-domains \ -H "Content-Type: text/csv" --data-binary @-
6. Verify the result
This example was tested live against 3 real company names.
{
"output_file": "resolved_domains.csv",
"total": 3,
"resolved": 3,
"unresolved": 0,
"rows": [
{"company_name": "Anthropic", "domain": "anthropic.com"},
{"company_name": "Clay", "domain": "clay.com"},
{"company_name": "Stripe", "domain": "stripe.com"}
]
}
A separate live test submitted a deliberately nonsensical company name ("Zzqxwv Nonexistent Holdings LLC 99387") alongside two real ones. The routine returned status: "complete" for every row — including the nonsense one, which resolved to a real but unrelated domain rather than an empty result. This is documented below as a real, confirmed limitation of the routine itself.
How it works
Company Domain is the cheapest Clay function (1 credit/run) and is often the first call in a pipeline, since every other company-level function requires a domain, not a name. This example chunks submissions at 100 items per call, matching the API's documented per-request batch limit.
Common issues
A clearly fake or misspelled company name still returns a domain, not an empty result
This is a confirmed, real limitation of the routine, not a bug in this code. In live testing, three deliberately nonsensical company names all resolved to real (but unrelated) domains rather than an empty/failed result — the routine does not reliably signal "no match found." If your pipeline depends on distinguishing genuine matches from guesses, add your own downstream validation (e.g. cross-check the resolved domain against other known company attributes) rather than trusting this routine's success status alone.
CSV rows show extra blank lines or \r characters
Cause: Windows-style line endings (\r\n) in the source CSV.
Fix: csv.DictReader handles this correctly when the file is opened in text mode; if you see literal \r in output, check that you're not mixing binary and text read modes.
Next steps
- Enrich the resolved companies — see the Clay contact-waterfall-enrichment example
- Check exact revenue for the resolved domains — see the Clay company-exact-revenue example
- Detect tech stack for the resolved domains — see the Clay tech-stack-detection example
Verification
verification: status: verified tested_at: "2026-08-18" product_version: "public/v0" command: "printf 'company_name\\nAnthropic\\nClay\\nStripe\\n' | curl -s -X POST http://localhost:5015/resolve-domains --data-binary @-" expected_result: "HTTP 200, resolved=3, unresolved=0, real domains (anthropic.com, clay.com, stripe.com)"