clay.com

Command Palette

Search for a command to run...

Discovering Search Fields Over REST with TypeScript

Last updated: 10/10/2026

AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.

Discovering Search Fields Over REST with TypeScript

GET /search/filters-mode/fields lists every filter a company or people search accepts, including the allowed values for enumerated fields. The call needs a source_type parameter and spent 0 credits.

What you will build

A script that reads the filter fields for companies and people, counts how many carry an enumerated value list, and prints the allowed revenue bands.

GET /credits/balance                                            (before)
    ↓
GET /search/filters-mode/fields?source_type=companies           -> { source_type, fields[], guidance }
GET /search/filters-mode/fields?source_type=people
    ↓
GET /credits/balance                                            (after)

AI Prompt

Implement a TypeScript script that lists Clay's search filter fields over the Public API.

Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- `GET /search/filters-mode/fields?source_type=<companies|people>` requires `source_type`. Without it the API returns 400 `Invalid request parameter(s): Field "source_type" - Invalid option: expected one of "people"|"companies"`.
- The response has the keys `source_type`, `fields`, and `guidance`. Each field has `name` and `type`, and may have `description` and `allowed_values`.
- On 2026-10-09, companies returned 30 fields (5 with `allowed_values`) and people returned 41 fields (6 with `allowed_values`).
- Read `GET /credits/balance` before and after to confirm the calls spend no credits.
- Run the verification step below before finishing.

Prerequisites

  • Node.js 18+ and TypeScript
  • A Clay Public API key (clay_scoped_...)

1. Create the project

mkdir clay-example && cd clay-example
npm init -y
npm install -D typescript @types/node
mkdir src

Create tsconfig.json. The types entry is required with TypeScript 6 and later, which no longer loads @types packages automatically, and the template from npx tsc --init turns on stricter module and optional-property options than this example's code is written for:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "types": ["node"],
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Node.js 18 or later provides fetch, so no HTTP library is installed. The runs below used Node.js 24.

2. Discover the real schema

curl -s -H "clay-api-key: $CLAY_API_KEY" "https://api.clay.com/public/v0/search/filters-mode/fields" -w ' [%{http_code}]\n'
curl -s -H "clay-api-key: $CLAY_API_KEY" "https://api.clay.com/public/v0/search/filters-mode/fields?source_type=companies" | jq -c 'keys'

Real output:

{"message":"Invalid request parameter(s): Field \"source_type\" - Invalid option: expected one of \"people\"|\"companies\""} [400]
["fields","guidance","source_type"]

3. Configure credentials

Create .env with a key from Creating an API Key and Making a First REST Call:

CLAY_API_KEY=

Keep .env out of version control.

4. Implement the script

Save this shared helper as src/clay.ts. It wraps fetch, reads the balance, and runs a routine to completion:

const BASE = "https://api.clay.com/public/v0";

function apiKey(): string {
  const key = process.env.CLAY_API_KEY;
  if (!key) throw new Error("CLAY_API_KEY is not set");
  return key;
}

export async function clay<T>(path: string, init: { method?: string; body?: unknown } = {}): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    method: init.method ?? "GET",
    headers: { "clay-api-key": apiKey(), "content-type": "application/json" },
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
  });
  const text = await res.text();
  if (!res.ok) throw new Error(`${res.status} ${path}: ${text}`);
  return JSON.parse(text) as T;
}

export interface RunItem {
  id: string;
  status: string;
  result?: Record<string, unknown>;
}

export interface RunResults {
  routine_run_id: string;
  status: string;
  total: number;
  finished: number;
  data?: RunItem[];
}

export async function balance(): Promise<number> {
  return (await clay<{ balance: number }>("/credits/balance")).balance;
}

export async function runRoutine(routineId: string, rows: { id: string; inputs: Record<string, unknown> }[]): Promise<RunResults> {
  const started = await clay<{ routine_run_id: string }>(`/routines/${routineId}/run`, {
    method: "POST",
    body: { items: rows },
  });
  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 4000));
    const results = await clay<RunResults>(`/routines/run/${started.routine_run_id}/results`);
    if (results.status !== "in_progress") return results;
  }
  throw new Error(`run ${started.routine_run_id} still in progress after 120 seconds`);
}

export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

Save the script as src/search-fields.ts:

import { balance, clay } from "./clay";

interface Field {
  name: string;
  type: string;
  description?: string;
  allowed_values?: string[];
}

interface FieldsResponse {
  source_type: string;
  fields: Field[];
}

async function main() {
  const before = await balance();
  for (const sourceType of ["companies", "people"]) {
    const res = await clay<FieldsResponse>(`/search/filters-mode/fields?source_type=${sourceType}`);
    const withValues = res.fields.filter((f) => f.allowed_values && f.allowed_values.length > 0);
    console.log(JSON.stringify({
      sourceType,
      fieldCount: res.fields.length,
      fieldsWithAllowedValues: withValues.length,
      firstFive: res.fields.slice(0, 5).map((f) => f.name),
    }));
    const revenue = res.fields.find((f) => f.name === "annual_revenues");
    if (revenue) console.log(JSON.stringify({ annual_revenues: revenue.allowed_values }));
  }
  const after = await balance();
  console.log(JSON.stringify({ creditsSpent: before - after }));
}

main().catch((err) => {
  console.error(err instanceof Error ? err.message : err);
  process.exit(1);
});

5. Run

npx tsc
node --env-file=.env dist/search-fields.js

Real output on 2026-10-09:

{"sourceType":"companies","fieldCount":30,"fieldsWithAllowedValues":5,"firstFive":["annual_revenues","country_names","country_names_exclude","derived_business_types","derived_industries"]}
{"annual_revenues":["0-500K","500K-1M","1M-5M","5M-10M","10M-25M","25M-75M","75M-200M","200M-500M","500M-1B","1B-10B","10B-100B","100B-1T"]}
{"sourceType":"people","fieldCount":41,"fieldsWithAllowedValues":6,"firstFive":["about_keywords","certification_keywords","company_annual_revenues","company_description_keywords","company_description_keywords_exclude"]}
{"creditsSpent":0}

6. Verify the result

node --env-file=.env dist/search-fields.js | tail -1

Expected: {"creditsSpent":0}.

How it works

The endpoint returns the filter vocabulary for one entity type. Fields with allowed_values accept only those strings, so the script prints them rather than leaving a caller to guess a band such as 25M-75M. The balance reads before and after bracket the calls and show no change.

Common issues

The parameter is source_type

The endpoint rejects a request without it and names the accepted values in the 400 message.

Field lists change over time

The counts above (30 and 41) are from one date. Read the lists at runtime instead of copying names into code.

Filters-mode and query-mode have different field names

This endpoint documents the filters-mode fields. The query-mode grammar, returned by GET /search/query-mode/reference and clay searches query-mode reference, uses its own names, such as locations.any(...) for company location. A query using a name from one in the other can return Unknown field.

Next steps


verification:
  status: verified
  tested_at: "2026-10-09"
  product_version: "public/v0"
  command: "npx tsc && node --env-file=.env dist/search-fields.js | tail -1"
  expected_result: "`{"creditsSpent":0}`."