Reference
Ingest API
Hardware-bound inference libraries publish declarative benchmark specs to the Hub over plain HTTP + JSON. A spec is a list of whitelisted UI nodes, never raw markup, so a publisher describes what to show and the Hub decides how to render it. This page documents every endpoint, its schemas and errors, and the full component whitelist.
Overview
- Base path
/api/v1. All requests and responses are JSON. - Two resources, projects and their benchmarks. Both writes are upserts keyed on
slug: POST the same slug again to update in place. - There is intentionally no delete endpoint. Deletion is a human, UI-only action; the API can only create, update, and read.
- Every benchmark
specis validated against the component whitelist before storage. Nothing partial is ever persisted.
Authentication
Authenticate with a Hub-issued API key sent as a Bearer token on every request:
Authorization: Bearer <your-api-key>The key is an opaque random token; the Hub stores only its SHA-256 hash, so a database read can never reconstruct a live credential. A missing or unrecognized token returns 401. Keys belong to one of two account types, library (automated publishers) or human, which the authorization rules below distinguish.
Endpoints
/api/v1/projectsList every project, ordered by name.
- Auth
- Any valid API key
- Returns
- ProjectOut[]
/api/v1/projectsCreate a project, or update one you own. Keyed on slug (upsert).
- Auth
- Any valid API key to create; owner or a human to update
- Body
- ProjectIn
- Returns
- ProjectOut
- Creating a new slug makes the caller its owner.
- Ownership is never reassigned on a later update.
/api/v1/projects/{project}/benchmarksList a project's benchmarks, newest run first.
- Auth
- Any valid API key
- Returns
- BenchmarkOut[]
- 404 if the project slug does not exist.
/api/v1/projects/{project}/benchmarksPublish a benchmark into a project. Keyed on slug (upsert).
- Auth
- Project owner or a human
- Body
- BenchmarkIn
- Returns
- BenchmarkOut
- The render spec is validated against the whitelist before anything is stored; an invalid node returns 422 and persists nothing.
- Each publish stamps last_run_at with the server time.
{project}in a path is the parent project's slug.
Schemas
Request bodies are strict; any unexpected key is rejected with a 422. A ? marks an optional field.
ProjectIn
requestExtra keys are rejected (strict schema). Convention: one project per hardware × model pair; each run publishes a new benchmark into it.
ProjectOut
responseBenchmarkIn
requestExtra keys are rejected (strict schema).
BenchmarkOut
responseErrors
Errors use conventional HTTP status codes and a JSON body with a message. Validation failures (422) additionally carry an errors array of issues, each pointing at the offending path.
A 422 from a spec that fails whitelist validation looks like:
{
"message": "invalid render spec",
"errors": [
{ "code": "custom", "path": ["3", "rows", 2],
"message": "row 2 has 4 cells but there are 3 headers" }
]
}Limits
- Request body: up to
8 MB. A larger declaredContent-Lengthis rejected with422before parsing. - Spec size: up to
2000top-level nodes. - Nesting depth: layout containers (
row,columns,tabs) may nest up to32levels deep. - Images:
srcmust be anhttp(s)://URL or adata:image/URI, up to 5 MB of characters. - Per-field caps (string lengths, array sizes) are listed with each node in the whitelist below.
Component whitelist
A spec is a JSON array of nodes. Each node is an object discriminated on a literal type. Only the types below are accepted; any other type, or any extra key on a node, is rejected. There is no raw-HTML or script node; this whitelist is the injection boundary.
Text
headerA section heading.
- text: string (1–300)
- level?: int 1–6 (default 2)
paragraphA block of prose.
- text: string (1–20000)
noteA callout box.
- text: string (1–20000)
- variant?: "info" | "warning" | "success" | "error" (default "info")
- title?: string (≤ 200)
code_blockA fenced code block.
- code: string (≤ 100000)
- language?: string (≤ 40, default "text")
- caption?: string (≤ 300)
dividerA horizontal rule.
- (no fields)
Data
tableA data table. Every row must have exactly as many cells as there are headers.
- headers: string[] (1–50)
- rows: Cell[][] (≤ 10000; each row length = headers length)
- caption?: string (≤ 300)
- Cell = string | number | boolean | null
key_valueA definition list.
- items: { key: string (1–200), value: string (≤ 2000) }[] (1–100)
- title?: string (≤ 200)
kpi_rowA row of metric tiles.
- metrics: Metric[] (1–12)
- Metric = { label: string (1–120), value: string (≤ 60), unit?: string (≤ 30), delta?: string (≤ 60), delta_direction?: "up" | "down" | "neutral" (default "neutral") }
imageAn image. No arbitrary markup; src is constrained.
- src: string, an http(s):// URL or a data:image/ URI (≤ 5,000,000 chars)
- alt?: string (≤ 300)
- caption?: string (≤ 300)
Charts
line_chartOne or more x/y line series.
- series: { name: string (1–120), x: number[] (1–100000), y: number[] (same length as x) }[] (1–30)
- x_label?: string (default "x")
- y_label?: string (default "y")
- title?: string (≤ 200)
bar_chartGrouped bars over shared categories.
- categories: string[] (1–1000)
- series: { name: string, values: number[] (same length as categories, ≤ 1000) }[] (1–30)
- x_label?: string (default "")
- y_label?: string (default "")
- title?: string (≤ 200)
scatter_chartLabeled x/y point clouds.
- series: { name: string, points: { x: number, y: number, label?: string }[] (1–100000) }[] (1–30)
- x_label?: string (default "x")
- y_label?: string (default "y")
- title?: string (≤ 200)
roofline_chartA roofline model: peak compute + bandwidth ceilings with plotted kernels.
- peak_flops: number (> 0)
- peak_bandwidth: number (> 0)
- points?: { label: string (1–120), intensity: number (> 0), performance: number (> 0) }[] (≤ 1000, default [])
- title?: string (≤ 200)
Layout (recursive)
rowLay children out horizontally.
- children: Node[] (1–12)
columnsFixed columns, each a stack of nodes.
- columns: Node[][] (1–6)
tabsTabbed panels.
- tabs: { label: string (1–120), children: Node[] (1–200) }[] (1–20)
Fields marked ?are optional; every default shown is what the Hub applies when the field is omitted. Charts enforce cross-field consistency (e.g. a line series' x and y must be the same length) and report a 422 pointing at the mismatch.
Examples
Structured publishing registers metrics, creates a campaign and run, then appends iterations containing timestamped measurements and values.
cURL
# Register a metric, then retain the returned id.
curl -X POST https://benchmark.infinity.inc/api/v1/metrics \
-H "Authorization: Bearer $HUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"key":"decode-throughput","name":"Decode throughput",
"unit":"token/s","direction":"higher_is_better"}'
curl -X POST https://benchmark.infinity.inc/api/v1/campaigns \
-H "Authorization: Bearer $HUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"qwen-neuralchip-x1","name":"Qwen on NeuralChip X1",
"model":"Qwen 3.5 9B","hardware":"NeuralChip X1"}'
# Use the returned campaign, run, and metric UUIDs in subsequent requests.
curl -X POST https://benchmark.infinity.inc/api/v1/campaigns/$CAMPAIGN_ID/runs \
-H "Authorization: Bearer $HUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"July run","workload":"interactive-decode",
"harness_version":"1.12.0"}'
curl -X POST https://benchmark.infinity.inc/api/v1/runs/$RUN_ID/iterations \
-H "Authorization: Bearer $HUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"iteration_number":0,"phase":"baseline",
"observed_at":"2026-07-22T12:00:00.000Z",
"measurements":[{"name":"interactive-decode",
"observed_at":"2026-07-22T12:00:00.000Z",
"values":[{"metric_id":"'"$METRIC_ID"'","unit":"token/s","value":120}]}]}'TypeScript SDK
@infinity/benchmark-hub-client is an ESM-only package exposing the typed, dependency-free HubClient. Identical iteration retries return the persisted iteration with created set to false.
import { HubClient } from "@infinity/benchmark-hub-client";
const hub = new HubClient("https://benchmark.infinity.inc", process.env.HUB_API_KEY!);
const metric = await hub.registerMetric({
key: "decode-throughput", name: "Decode throughput",
unit: "token/s", direction: "higher_is_better",
});
const campaign = await hub.resolveCampaign("qwen-neuralchip-x1")
?? await hub.createCampaign({
slug: "qwen-neuralchip-x1", name: "Qwen on NeuralChip X1",
model: "Qwen 3.5 9B", hardware: "NeuralChip X1",
});
const run = await hub.createRun(campaign.id, {
name: "July run", workload: "interactive-decode", harness_version: "1.12.0",
});
await hub.transitionRun(run.id, { status: "active" });
const observedAt = new Date().toISOString();
const { iteration, created } = await hub.appendIteration(run.id, {
iteration_number: 0, phase: "baseline", observed_at: observedAt,
measurements: [{ name: "interactive-decode", observed_at: observedAt,
values: [{ metric_id: metric.id, unit: metric.unit, value: 120 }],
}],
});
console.log(created ? "created" : "identical retry", iteration.id);Python
Publishers on ML hardware don't need the SDK; the structured API accepts plain JSON over HTTP.
import os, httpx
HUB = "https://benchmark.infinity.inc"
h = {"Authorization": f"Bearer {os.environ['HUB_API_KEY']}",
"Content-Type": "application/json"}
metric = httpx.post(f"{HUB}/api/v1/metrics", headers=h, json={
"key": "decode-throughput", "name": "Decode throughput",
"unit": "token/s", "direction": "higher_is_better",
}).raise_for_status().json()
campaign = httpx.post(f"{HUB}/api/v1/campaigns", headers=h, json={
"slug": "qwen-neuralchip-x1", "name": "Qwen on NeuralChip X1",
"model": "Qwen 3.5 9B", "hardware": "NeuralChip X1",
}).raise_for_status().json()
run = httpx.post(f"{HUB}/api/v1/campaigns/{campaign['id']}/runs", headers=h,
json={"name": "July run", "workload": "interactive-decode",
"harness_version": "1.12.0"}).raise_for_status().json()
observed_at = "2026-07-22T12:00:00.000Z"
iteration = httpx.post(f"{HUB}/api/v1/runs/{run['id']}/iterations", headers=h, json={
"iteration_number": 0, "phase": "baseline", "observed_at": observed_at,
"measurements": [{"name": "interactive-decode", "observed_at": observed_at,
"values": [{"metric_id": metric["id"],
"unit": metric["unit"], "value": 120}]}],
}).raise_for_status().json()