This quickstart creates a simple query run, waits for it to finish, and fetches the resulting table artifact.
1. Create a Run
curl -X POST https://havilo.ai/api/v1/runs \
-H "Authorization: Bearer $HAVILO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"input_type": "query",
"query": "list 3 climate-tech startups in Europe",
"config": {
"strict_mode": true
}
}'Successful responses return 202 Accepted:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "run",
"status": "pending",
"api_version": "2026-04-22",
"created_at": 1714500000000,
"workspace_id": "8b3d0c7a-4ef5-4f11-9e8a-8d3b08c29c10",
"resumable": false,
"request_id": "req_01hxbexample",
"input": {
"input_type": "query",
"query": "list 3 climate-tech startups in Europe",
"config": {
"strict_mode": true
}
},
"config": {
"strict_mode": true
}
}2. Poll the Run
curl https://havilo.ai/api/v1/runs/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $HAVILO_API_KEY"When the run succeeds, the envelope includes completed steps and artifact metadata:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "run",
"status": "succeeded",
"api_version": "2026-04-22",
"created_at": 1714500000000,
"completed_at": 1714500018000,
"workspace_id": "8b3d0c7a-4ef5-4f11-9e8a-8d3b08c29c10",
"input": {
"input_type": "query",
"query": "list 3 climate-tech startups in Europe",
"config": {
"strict_mode": true
}
},
"config": {
"strict_mode": true
},
"steps": [
{
"index": 0,
"kind": "fetch",
"status": "completed",
"title": "Find climate-tech startups",
"duration_ms": 1870,
"items_out": 3
},
{
"index": 1,
"kind": "reconstitute",
"status": "completed",
"title": "Format results",
"duration_ms": 950
}
],
"message": "Here are 3 climate-tech startups.",
"artifacts": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"kind": "table",
"title": "Climate-Tech Startups",
"row_count": 3
}
],
"warnings": [],
"errors": [],
"usage": {
"calls": 4,
"unpriced_calls": 0,
"input_tokens": 12000,
"cached_input_tokens": 6000,
"output_tokens": 1800,
"thinking_tokens": 300,
"cost_usd": 0.02
},
"resumable": false,
"request_id": "req_01hxbexample"
}3. Fetch the Artifact
curl https://havilo.ai/api/v1/artifacts/550e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer $HAVILO_API_KEY"{
"id": "550e8400-e29b-41d4-a716-446655440001",
"object": "artifact",
"kind": "table",
"title": "Climate-Tech Startups",
"run_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": 1714500018000,
"row_count": 3,
"columns": ["Name", "Description", "Website", "Location"],
"rows": [
{
"Name": "Example Startup",
"Description": "A short company description.",
"Website": "https://example.com",
"Location": "San Francisco, CA"
}
]
}TypeScript Example
A complete client with a stable idempotency key, terminal-state polling with exponential backoff, and structured error extraction.
const BASE = 'https://havilo.ai/api/v1'
const TERMINAL = new Set(['succeeded', 'failed', 'canceled'])
type Run = {
id: string
status: 'pending' | 'running' | 'paused' | 'succeeded' | 'failed' | 'canceled'
message?: string
artifacts?: Array<{ id: string; kind: string; title: string }>
errors?: Array<{ code: string; message: string }>
}
async function createRun(apiKey: string, query: string, idempotencyKey: string): Promise<Run> {
const response = await fetch(`${BASE}/runs`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
// Reuse this value for every retry of the same logical create request.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
input_type: 'query',
query,
config: { strict_mode: true },
}),
})
if (!response.ok) {
const body = await response.json().catch(() => null)
const code = body?.error?.code ?? `http_${response.status}`
const message = body?.error?.message ?? response.statusText
throw new Error(`${code}: ${message}`)
}
return response.json()
}
async function pollRun(apiKey: string, runId: string): Promise<Run> {
let delayMs = 1000
while (true) {
await new Promise((r) => setTimeout(r, delayMs))
const response = await fetch(`${BASE}/runs/${runId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
if (!response.ok) throw new Error(`poll failed: ${response.status}`)
const run: Run = await response.json()
if (TERMINAL.has(run.status)) return run
delayMs = Math.min(delayMs * 1.5, 5000) // cap at 5s
}
}
async function getArtifact<T = unknown>(apiKey: string, artifactId: string): Promise<T> {
const response = await fetch(`${BASE}/artifacts/${artifactId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
if (!response.ok) throw new Error(`artifact fetch failed: ${response.status}`)
return response.json()
}
async function main() {
const apiKey = process.env.HAVILO_API_KEY!
const query = 'list 3 climate-tech startups in Europe'
const idempotencyKey = `query-${crypto.randomUUID()}`
const created = await createRun(apiKey, query, idempotencyKey)
const run = await pollRun(apiKey, created.id)
if (run.status !== 'succeeded') {
const first = run.errors?.[0]
throw new Error(first ? `${first.code}: ${first.message}` : `run ${run.status}`)
}
console.log(run.message)
for (const artifact of run.artifacts ?? []) {
const body = await getArtifact(apiKey, artifact.id)
console.log(artifact.kind, body)
}
}