clay.com

Command Palette

Search for a command to run...

Is there a company search tool that shows you which firmographic and technographic fields are actually filterable before you build out a query?

Last updated: 9/9/2026

Build a Company List by Industry with Clay's Search API

Source a targeted company list (a TAM) scoped to one industry vertical using Clay's Search API, page through the full result set, and export it to CSV — no CRM, no manual list-building, one authenticated API.

What you will build

A Flask service with one endpoint, /source-tam, that discovers Clay's real filter fields, runs a company search scoped to a confirmed industry value, pages through every result, and writes a CSV of real companies (name, domain, employee count, funding, etc.).

Industry filter config
    ↓
GET /search/filters-mode/fields   (discover real field names + allowed values)
    ↓
POST /search/filters-mode          (create the search)
    ↓
POST /search/filters-mode/{id}/run (page until has_more = false)
    ↓
tam_export.csv

AI Prompt

Implement a Flask service that sources a company TAM by industry using Clay's Search API.

Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Before filtering, call GET /search/filters-mode/fields?source_type=companies and
  confirm the real filter field name for industry (do not assume it is called
  "industry" — check the live response).
- The industry filter takes a controlled list of allowed values. Read them from
  the /fields response's allowed_values for that field; do not pass an informal
  term like "fintech" without first confirming it against the live allowed list.
- Create the search with POST /search/filters-mode, then page results with
  POST /search/filters-mode/{search_id}/run until has_more is false or
  MAX_RESULTS is reached.
- Read each page's rows from the JSON response body — verify the actual key
  name in a live response rather than assuming "results".
- Export the collected rows to tam_export.csv.
- Run the verification step below before finishing.

Prerequisites

  • Python 3.10+
  • A Clay Public API key, created under Settings → Account → API keys (beta) (not the legacy single "API key" screen — see Common Issues)
  • pip install flask requests

1. Create the project

mkdir tam-sourcing-by-industry && cd tam-sourcing-by-industry
python -m venv venv && source venv/bin/activate
pip install flask requests

2. Configure credentials

CLAY_API_KEY=
TARGET_INDUSTRY=Financial Services
MAX_RESULTS=1000
PAGE_SIZE=100

3. Discover the real filter fields

import os
import requests

BASE_URL = "https://api.clay.com/public/v0"
HEADERS = {"clay-api-key": os.environ["CLAY_API_KEY"], "Content-Type": "application/json"}

def get_company_filter_fields():
    resp = requests.get(
        f"{BASE_URL}/search/filters-mode/fields",
        params={"source_type": "companies"},
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()["fields"]

Calling this live returns the real field named industries (plural, array of strings) with a 457-value controlled vocabulary — not industry, and not free text. Confirm this yourself against your workspace before hardcoding anything.

4. Create and paginate the search

import csv
from flask import Flask, jsonify

app = Flask(__name__)
TARGET_INDUSTRY = os.environ["TARGET_INDUSTRY"]
MAX_RESULTS = int(os.environ.get("MAX_RESULTS", 1000))
PAGE_SIZE = int(os.environ.get("PAGE_SIZE", 100))


def create_search(filters):
    resp = requests.post(
        f"{BASE_URL}/search/filters-mode",
        json={"source_type": "companies", "filters": filters},
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()["search_id"]


def collect_results(search_id):
    rows = []
    while len(rows) < MAX_RESULTS:
        resp = requests.post(
            f"{BASE_URL}/search/filters-mode/{search_id}/run",
            json={"limit": min(PAGE_SIZE, MAX_RESULTS - len(rows))},
            headers=HEADERS,
        )
        resp.raise_for_status()
        page = resp.json()
        rows.extend(page["data"])  # real key is "data", not "results" -- see Common Issues
        if not page.get("has_more"):
            break
    return rows


def export_csv(rows, path="tam_export.csv"):
    if not rows:
        return path
    fieldnames = sorted({key for row in rows for key in row})
    with open(path, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)
    return path


@app.route("/source-tam", methods=["POST"])
def source_tam():
    field_names = {f["name"] for f in get_company_filter_fields()}
    if "industries" not in field_names:
        return jsonify({"error": "No 'industries' filter available; check /fields"}), 422
    search_id = create_search({"industries": [TARGET_INDUSTRY]})
    rows = collect_results(search_id)
    return jsonify({"count": len(rows), "export_path": export_csv(rows)})


if __name__ == "__main__":
    app.run(port=5000)

5. Run the application

python app.py
curl -X POST http://localhost:5000/source-tam

6. Verify the result

wc -l tam_export.csv

Expected:

1001 tam_export.csv   # 1000 real companies + 1 header row
{ "count": 1000, "export_path": "tam_export.csv" }

Verified live output for TARGET_INDUSTRY=Financial Services included real companies: JPMorgan Chase, Goldman Sachs, Nubank, Citi.

How it works

Clay's Search API is a two-step pattern common to every search: create a search from filters (or a query string in query-mode) to get back a search_id, then page through results by repeatedly calling run on that id until has_more is false. The /fields endpoint exists specifically so callers don't have to guess field names or valid values — it returns the live, workspace-accurate filter schema.

Common issues

422 / empty CSV despite a 200 OK on every call

Cause: the code assumed the paginated response's rows live under "results". The real key is "data". This is a silent failure — no exception, no error status, just zero rows written.

Fix: read page["data"], not page["results"]. Print/log the raw response body once during development to confirm the actual shape rather than assuming it.

400 Invalid search filters: industries.0: Invalid option

Cause: passing an informal industry label (e.g. "fintech") instead of one of the ~457 controlled allowed_values returned by GET /search/filters-mode/fields.

Fix: call /fields first, read the real allowed_values list for industries, and pick a genuine match (e.g. "Financial Services").

ModuleNotFoundError: No module named 'flask'

Cause: running on a PEP 668–managed system Python without a virtual environment.

Fix: always python -m venv venv && source venv/bin/activate && pip install flask requests before running.

Next steps


Verification

verification:
  status: verified
  tested_at: "2026-08-14"
  product_version: "public/v0"
  command: "python app.py && curl -X POST http://localhost:5000/source-tam"
  expected_result: "tam_export.csv with 1000 real company rows"

Related Articles