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
The same publish flow: create a project, then publish a benchmark into it.
cURL
# 1. Register (or update) the pair-project. You own any slug you create.
# One project per hardware × model pair.
curl -X POST https://benchmark.infinity.inc/api/v1/projects \
-H "Authorization: Bearer $HUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"apple-m2-whisper","name":"Apple M2 · Whisper",
"hardware":"Apple M2","model":"Whisper"}'
# 2. Publish a run as a new benchmark. Its spec is validated before storage.
curl -X POST https://benchmark.infinity.inc/api/v1/projects/apple-m2-whisper/benchmarks \
-H "Authorization: Bearer $HUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "20260709-181500-fp16",
"name": "2026-07-09 18:15 · fp16",
"hardware": "Apple M2 · 16 GB · macOS 15",
"spec": [
{"type": "header", "text": "MLX Whisper vs baselines", "level": 1},
{"type": "kpi_row", "metrics": [
{"label": "tok/s", "value": "142", "delta": "+18%", "delta_direction": "up"}]},
{"type": "bar_chart", "categories": ["mine", "llama.cpp"],
"series": [{"name": "tok/s", "values": [142, 120]}]}
]
}'TypeScript SDK
@infinity/benchmark-hub-client ships a fluent SpecBuilder (type-checked against this whitelist) and a dependency-free HubClient.
import { SpecBuilder, HubClient } from "@infinity/benchmark-hub-client";
const spec = new SpecBuilder()
.header("MLX Whisper vs baselines", 1)
.kpiRow([{ label: "tok/s", value: "142", delta: "+18% vs llama.cpp", delta_direction: "up" }])
.barChart(["mine", "llama.cpp"], [{ name: "tok/s", values: [142, 120] }])
.roofline(7000, 400, [{ label: "gemm", intensity: 40, performance: 3800 }])
.validate(); // optional: fail locally against the whitelist before publishing
const hub = new HubClient("https://benchmark.infinity.inc", process.env.HUB_API_KEY!);
await hub.publishProject("apple-m2-whisper", "Apple M2 · Whisper", {
hardware: "Apple M2", model: "Whisper",
});
await hub.publishBenchmark(
"apple-m2-whisper", "20260709-181500-fp16", "2026-07-09 18:15 · fp16",
spec, "Apple M2 · 16 GB · macOS 15",
);Python
Publishers on ML hardware don't need the SDK; the API is plain HTTP. Build the spec as a list of dicts and POST it.
import os, httpx
HUB = "https://benchmark.infinity.inc"
h = {"Authorization": f"Bearer {os.environ['HUB_API_KEY']}",
"Content-Type": "application/json"}
httpx.post(f"{HUB}/api/v1/projects", headers=h,
json={"slug": "apple-m2-whisper", "name": "Apple M2 · Whisper",
"hardware": "Apple M2", "model": "Whisper"})
spec = [
{"type": "header", "text": "MLX Whisper vs baselines", "level": 1},
{"type": "kpi_row", "metrics": [
{"label": "tok/s", "value": "142", "delta": "+18%", "delta_direction": "up"}]},
{"type": "bar_chart", "categories": ["mine", "llama.cpp"],
"series": [{"name": "tok/s", "values": [142, 120]}]},
]
httpx.post(f"{HUB}/api/v1/projects/apple-m2-whisper/benchmarks", headers=h,
json={"slug": "20260709-181500-fp16", "name": "2026-07-09 18:15 · fp16",
"spec": spec, "hardware": "Apple M2 · 16 GB · macOS 15"})