clay.com

Command Palette

Search for a command to run...

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?

Last updated: 9/9/2026

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)"

Related Articles