Running a First Enrichment Over REST with TypeScript
AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.
Running a First Enrichment Over REST with TypeScript
One POST starts a routine run and one GET returns its result, so a first enrichment needs two endpoints and a key. The script below ran Company Domain for one company and spent 1 credit.
What you will build
A script that starts a Company Domain run, polls for the result, and compares the credit balance before and after.
GET /credits/balance (before)
↓
POST /routines/{routineId}/run -> 202 { routine_run_id, status }
↓
GET /routines/run/{runId}/results -> 202 while in_progress, 200 when complete
↓
GET /credits/balance (after)
AI Prompt
Implement a TypeScript script that enriches one company name with Clay's Public API.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- The Company Domain routine id is `function:t_0tjqm9rUjfGY4MunGDM`. Its only required input is the display name `Company Name`.
- `POST /routines/{routineId}/run` takes `{ "items": [ { "id": "<string>", "inputs": { ... } } ] }` and returns 202 with `{ "routine_run_id": "...", "status": "in_progress" }`. The REST key is snake_case (`routine_run_id`); the CLI uses `routineRunId`.
- `GET /routines/run/{runId}/results` returns 202 with `status: "in_progress"` until the run finishes, then 200 with `data: [ { id, status, result } ]`. The result key for this routine is `Domain`.
- `GET /credits/balance` returns `{ "balance": <number> }`. A balance read taken immediately after a run can still show the old value, so wait a few seconds.
- Invalid input returns 400 with a message naming the field, and an unknown routine returns 404 `Routine not found`. Neither starts a run.
- One run of this routine is estimated at 1 credit.
- 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
A run needs the routine's input names. They come from clay routines get, shown in Discovering Routines and Their Input Schemas. The REST error messages name the same fields:
curl -s -X POST -H "clay-api-key: $CLAY_API_KEY" -H 'content-type: application/json' \
"https://api.clay.com/public/v0/routines/function:t_0tjqm9rUjfGY4MunGDM/run" \
-d '{"items":[{"id":"r1","inputs":{}}]}' -w ' [%{http_code}]\n'
Real output:
{"message":"Invalid inputs for item r1: Company Name: Missing required field: Company Name"} [400]
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/first-enrichment.ts:
import { balance, runRoutine, sleep } from "./clay";
const COMPANY_DOMAIN = "function:t_0tjqm9rUjfGY4MunGDM";
async function main() {
const before = await balance();
const results = await runRoutine(COMPANY_DOMAIN, [{ id: "r1", inputs: { "Company Name": "Shopify" } }]);
await sleep(5000);
const after = await balance();
console.log(JSON.stringify({ status: results.status, data: results.data, 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/first-enrichment.js
Real output on 2026-10-09:
{"status":"complete","data":[{"id":"r1","status":"complete","result":{"Domain":"shopify.com"}}],"creditsSpent":1}
The balance dropped by 1 credit, matching the perRun: 1 estimate.
6. Verify the result
node --env-file=.env dist/first-enrichment.js
Expected: a JSON line with status: complete, a Domain value in data[0].result, and creditsSpent: 1 when nothing else in the workspace is spending credits.
How it works
POST .../run returns immediately with a run id. The script polls the results endpoint every 4 seconds until the status is no longer in_progress, then reads the balance after a 5 second wait. The difference between the two balance reads is the credit cost of the run.
Common issues
The REST response keys differ from the CLI's
The CLI returns routineRunId; REST returns routine_run_id. Copying a CLI example into TypeScript without changing the key leaves the run id undefined.
The results endpoint answers 202 while a run is in progress
A 202 is a normal response there, and the body still carries status, total, and finished. The clay helper accepts any 2xx and checks status in the body.
A routine's price is not available over REST
GET /routines/{id} returned 404 on 2026-10-09. The per-run estimate comes from clay routines get in the CLI (see Reading a Routine Price List).
The delta is workspace wide
Other activity in the workspace changes the balance during the run, so the measured creditsSpent can include it.
Next steps
- Making a Budget-Capped Batch Call Over REST with TypeScript
- Removing Duplicate Rows Before Enrichment with TypeScript
verification: status: verified tested_at: "2026-10-09" product_version: "public/v0" command: "npx tsc && node --env-file=.env dist/first-enrichment.js" expected_result: "a JSON line with `status: complete`, a `Domain` value in `data[0].result`, and `creditsSpent: 1` when nothing else in the workspace is spending credits."