Call OpenClay from your own code
The UI is one client of a small HTTP API. If you are wiring enrichment into a pipeline rather than clicking through a browser, POST to it directly.
Before you start
- Run your own instance. The hosted endpoint at openclay.io exists to serve the web app and carries no uptime promise for programmatic use. Self-hosting takes three commands. See the
/self-hostguide. - There is no OpenClay auth. The API forwards whatever provider key you send. Anyone who can reach your instance can spend your key, so do not expose it publicly without putting your own auth in front.
- One row per request. There is no batch endpoint by design, which keeps the server stateless. Fan out client-side with whatever concurrency your quota tolerates.
POST /api/enrich
Sends one prompt to one model and returns parsed JSON plus token usage.
Request
curl -X POST http://localhost:3000/api/enrich \
-H 'content-type: application/json' \
-d '{
"provider": "gemini",
"apiKey": "AIza, ...",
"modelId": "gemini-3-flash-preview",
"prompt": "Find the CEO of Stripe. Return ONLY JSON: {\"ceo\":\"string\"}",
"useWebSearch": true
}'| Field | Type | Notes |
|---|---|---|
| provider | string | anthropic | gemini | grok | openai | custom |
| apiKey | string | Your provider key. Optional only when provider is custom and the gateway needs none. |
| modelId | string | A model id from /models, or your own for a custom endpoint. |
| prompt | string | The fully-rendered prompt for one row. Ask for strict JSON. |
| useWebSearch | boolean | Defaults to true. Ignored by models without search. |
| baseUrl | string | Custom provider only. An OpenAI-compatible /v1 base URL. |
Response
{
"data": { "ceo": "Patrick Collison" },
"inputTokens": 1284,
"outputTokens": 31
}data is whatever JSON the model returned, flattened to string values so it maps cleanly onto spreadsheet cells. Nested objects are stringified rather than rendered as[object Object].
Errors
The upstream status is preserved rather than collapsed into a 500, because that distinction is what makes a retry loop possible.
429: rate limited. Retry with backoff.retryAfterMsis included when the provider sent a Retry-After header.5xx: transient provider failure, worth retrying.400 / 401 / 403 / 404: bad request, bad key, no access, unknown model. These fail identically on every attempt, so do not retry them.422: the model replied but not with usable JSON, or was cut off. Usually a prompt problem.
{ "error": "Provider rejected the request (HTTP 404): model not found" }POST /api/validate-key
Cheap probe to confirm a key works before spending real tokens on a batch.
curl -X POST http://localhost:3000/api/validate-key \
-H 'content-type: application/json' \
-d '{ "provider": "openai", "apiKey": "sk-..." }'
# -> { "valid": true }
# -> { "valid": false, "error": "Key rejected by the provider." }
# -> { "valid": true, "warning": "Key is valid but currently rate limited." }A 429 reports valid: true with a warning, because a rate limit proves the key is real. Any other 4xx reports valid: false with the provider's own message.
A worked example
Enriching a CSV from Node, with bounded concurrency and retry on 429:
import { readFileSync } from "node:fs";
const API = "http://localhost:3000/api/enrich";
const rows = JSON.parse(readFileSync("rows.json", "utf8"));
async function enrich(row) {
const prompt = `Find the CEO and funding of ${row.company}.
Return ONLY JSON: {"ceo":"string","funding":"string"}`;
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(API, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
provider: "gemini",
apiKey: process.env.GEMINI_API_KEY,
modelId: "gemini-3-flash-preview",
prompt,
}),
});
if (res.ok) return { ...row, ...(await res.json()).data };
// Only 429 and 5xx are worth another attempt.
if (res.status !== 429 && res.status < 500) {
throw new Error((await res.json()).error);
}
// Full jitter: random within a growing ceiling, so parallel
// workers don't all retry on the same tick.
await new Promise((r) => setTimeout(r, Math.random() * 1000 * 2 ** attempt));
}
throw new Error("exhausted retries");
}
// Keep concurrency modest; raise it once you know your quota.
const LIMIT = 3;
const out = [];
for (let i = 0; i < rows.length; i += LIMIT) {
out.push(...(await Promise.all(rows.slice(i, i + LIMIT).map(enrich))));
}
console.log(JSON.stringify(out, null, 2));Machine-readable summary
https://openclay.io/llms.txt carries a plain-text description of the project, the full model catalog with current prices, and a precise statement of scope, written for assistants and agents rather than browsers.
No account, no card, no platform fee.
Bring your own API key and pay the model provider directly.
Or use the web app