Geographic TAM Segmentation with TypeScript
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
industriesorannual_revenuesfilters 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."