clay.com

Command Palette

Search for a command to run...

Geographic TAM Segmentation with TypeScript

Last updated: 9/9/2026

Geographic TAM Segmentation with TypeScript

Segment a sourced TAM by real HQ region, country, state, or city — using Clay's Search API filters-mode, in a small TypeScript/Express service. (Note: Clay has no timezone filter; this segments on real geographic fields only.)

What you will build

An Express service exposing POST /segment-by-geography, which builds a companies search filtered on any combination of region, country, state, and city, optionally restricted to headquarters only.

POST /segment-by-geography (regions, countries, states, cities, headquartersOnly, maxResults)
    ↓
POST /search/filters-mode           (create, with real geographic filters)
    ↓
POST /search/filters-mode/{id}/run  (page results)
    ↓
companies[]

AI Prompt

Implement a TypeScript/Express service that segments a company TAM by
geography using Clay's Search API filters-mode.

Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Real geographic fields for companies (from
  GET /search/filters-mode/fields?source_type=companies, 30 fields total):
  country_names / country_names_exclude, location_regions_include /
  location_regions_exclude (e.g. "EMEA", "NAM"), location_states_include,
  location_cities_include, location_headquarters_only (boolean). None of
  these have allowed_values -- they are free text, and a typo silently
  returns 0 rows, not an error.
- There is NO timezone field anywhere in this catalog. Do not invent one
  or attempt to filter by timezone; segment on region/country/state/city
  only.
- Create the search with POST /search/filters-mode, run it with
  POST /search/filters-mode/{search_id}/run, and return the real company
  rows.
- 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 tam-geo-segment && cd tam-geo-segment
npm init -y
npm install express
npm install -D typescript @types/express @types/node
npx tsc --init

2. Discover the real schema

curl -s "https://api.clay.com/public/v0/search/filters-mode/fields?source_type=companies" \
  -H "clay-api-key: $CLAY_API_KEY"

Real geographic fields (of 30 total company fields): country_names, country_names_exclude, location_regions_include/_exclude, location_states_include/_exclude, location_cities_include/_exclude, location_postal_codes_include/_exclude, locations/locations_exclude, location_headquarters_only (boolean). No timezone field exists.

3. Configure credentials

CLAY_API_KEY=

In a CI/sandbox test environment, this is typically provided as a pre-configured secret; in your own deployment, set it as a real environment variable or via your platform's secrets manager.

4. Implement the service

import express, { Request, Response as ExpressResponse } from "express";

const CLAY_API_BASE = "https://api.clay.com/public/v0";
const CLAY_API_KEY = process.env.CLAY_API_KEY;

if (!CLAY_API_KEY) throw new Error("CLAY_API_KEY env var is required");

interface CompanyRow {
  name: string;
  domain: string;
  country: string;
  location: string;
  industry: string;
}

async function clayFetch(path: string, init: RequestInit = {}): Promise<globalThis.Response> {
  return fetch(`${CLAY_API_BASE}${path}`, {
    ...init,
    headers: {
      "clay-api-key": CLAY_API_KEY as string,
      "Content-Type": "application/json",
      ...(init.headers || {}),
    },
  });
}

interface GeoFilters {
  location_regions_include?: string[];
  location_regions_exclude?: string[];
  country_names?: string[];
  country_names_exclude?: string[];
  location_states_include?: string[];
  location_cities_include?: string[];
  location_headquarters_only?: boolean;
}

// Search filters-mode, filtered on real geographic fields only. There is NO
// timezone field in Clay's companies filter catalog (30 real
// fields captured live, none timezone-related) -- this segments by region/
// country/state/city and an HQ-only toggle, not by timezone.
async function segmentByGeography(filters: GeoFilters, limit: number): Promise<CompanyRow[]> {
  const createRes = await clayFetch("/search/filters-mode", {
    method: "POST",
    body: JSON.stringify({ source_type: "companies", filters }),
  });
  if (!createRes.ok) {
    throw new Error(`search create failed: ${createRes.status} ${await createRes.text()}`);
  }
  const { search_id } = (await createRes.json()) as { search_id: string };

  const runRes = await clayFetch(`/search/filters-mode/${search_id}/run`, {
    method: "POST",
    body: JSON.stringify({ limit }),
  });
  if (!runRes.ok) {
    throw new Error(`search run failed: ${runRes.status} ${await runRes.text()}`);
  }
  const { data } = (await runRes.json()) as { data: CompanyRow[] };
  return data;
}

const app = express();
app.use(express.json());

app.post("/segment-by-geography", async (req: Request, res: ExpressResponse) => {
  const {
    regions,
    excludeRegions,
    countries,
    excludeCountries,
    states,
    cities,
    headquartersOnly,
    maxResults = 20,
  } = req.body ?? {};

  if (!regions && !countries && !states && !cities) {
    return res
      .status(400)
      .json({ error: "at least one of regions, countries, states, or cities is required" });
  }

  const filters: GeoFilters = {};
  if (regions) filters.location_regions_include = regions;
  if (excludeRegions) filters.location_regions_exclude = excludeRegions;
  if (countries) filters.country_names = countries;
  if (excludeCountries) filters.country_names_exclude = excludeCountries;
  if (states) filters.location_states_include = states;
  if (cities) filters.location_cities_include = cities;
  if (headquartersOnly !== undefined) filters.location_headquarters_only = headquartersOnly;

  try {
    const companies = await segmentByGeography(filters, maxResults);
    res.json({ count: companies.length, companies });
  } catch (err) {
    res.status(502).json({ error: (err as Error).message });
  }
});

const port = Number(process.env.PORT) || 3000;
app.listen(port, () => console.log(`listening on ${port}`));

5. Run

npx tsc
node --env-file=.env dist/index.js
curl -X POST http://localhost:3000/segment-by-geography \
  -H "Content-Type: application/json" \
  -d '{"regions":["NAM"],"headquartersOnly":true,"maxResults":3}'

6. Verify the result

{
  "count": 3,
  "companies": [
    {"name": "Google", "domain": "google.com", "country": "United States", "location": "Mountain View, California, United States", "industry": "Software Development"},
    {"name": "LinkedIn", "domain": "linkedin.com", "country": "United States", "location": "Mountain View, California, United States", "industry": "Software Development"},
    {"name": "Microsoft", "domain": "microsoft.com", "country": "United States", "location": "Redmond, Washington, United States", "industry": "Software Development"}
  ]
}

All three are genuine NAM-headquartered companies — real data.

How it works

Geographic segmentation here is a single Search call, not a routine — Clay's companies filter catalog has real region/country/state/city fields, so there's no per-item cost the way a routine-based example has. The catch is scope: the field catalog has no timezone concept at all, so "segment by timezone" (a plausible-sounding ask) simply isn't buildable against Clay's real filters — only region, country, state, city, and an HQ-only toggle are.

Common issues

Filtering by timezone returns nothing or errors

Cause: assuming a timezone filter exists because it's a natural extension of "geographic segmentation."

Fix: it doesn't exist in the 30-field catalog for companies. Use region/country/state/city instead, or compute a timezone client-side from the country/location fields Clay does return.

A region or city name returns zero rows with no error

Cause: these fields are free text with no allowed_values — a typo or non-matching casing/spelling silently returns an empty result set rather than a validation error.

Fix: treat 0 results as a signal to double check the spelling/format, not as proof the segment is empty.

Next steps

  • Combine with industries or annual_revenues filters to narrow further within a region.
  • Feed the resulting domains into a routine (e.g. Website Technology Stack) for per-company enrichment — see the technographic TAM filtering example.

verification:
  status: verified
  tested_at: "2026-09-02"
  product_version: "public/v0"
  command: "npx tsc && node --env-file=.env dist/index.js && curl -X POST http://localhost:3000/segment-by-geography -d '{\"regions\":[\"NAM\"],\"headquartersOnly\":true,\"maxResults\":3}'"
  expected_result: "Three real NAM-headquartered companies returned (Google, LinkedIn, Microsoft), using only real geographic fields, with no timezone field used anywhere."

Related Articles