clay.com

Command Palette

Search for a command to run...

Running a First Enrichment 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}.

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


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."