The Semurg HTTP API.
Two JSON surfaces, both hand-rolled on the perimeter gateway. The /v1 data API puts your own
bytes into a licensed instance and reads them back. The /api benchmark API runs the same live
queries the demos run, so any number here is reproducible on your own hardware. Every endpoint on this
page was enumerated from the router and controllers and confirmed live against
one.semurg.io. Performance figures shown in the example payloads below are the
canonical stored snapshot — run 2026-08-18, build f279dfd, quiet box —
the same source served at /semurg-metrics.js and rendered on
Benchmarks. Example bodies show the
response shape; the authoritative numbers are that one snapshot.
Base URL and content types.
On the public reference box the API is served at https://one.semurg.io behind
Caddy, which proxies the JSON paths to the gateway on port 4000. A self-hosted instance answers on
http://localhost:4000 directly. There is no API versioning beyond the
/v1 prefix.
| Surface | Prefix | Purpose | Reads | Writes |
|---|---|---|---|---|
| Data API | /v1 | put your bytes in, read them out | open (auth optional) | licence write cap |
| Benchmark API | /api | reproduce the live numbers | open | blocked on public |
| Download gate | /d | issued-token bundle fetch | token only | n/a |
/v1 negotiates application/json for the JSON ops and
application/octet-stream ("bin") for the binary /v1/fetch_batch
fast path. /api accepts JSON only. Both surfaces are batch-first: one request
carries a batch and costs exactly one crossing into the engine. There is deliberately no per-item write
endpoint, because a per-item turnstile is the one shape that cannot reach the engine's throughput.
Three gates, all fail-open by construction.
Optional caller auth (bearer token)
The /v1 surface has an optional built-in authentication gate.
With no token provisioned it fails open: every /v1 request is served, which is
exactly the public read-only reference box. Once a token is set (env
SEMURG_API_TOKEN, an app-env :api_token, or an
api_token= licence capability) the gate fails closed: every
/v1 request, read and write alike, must present the token or get
401. The token is compared in constant time.
curl https://one.semurg.io/v1/status -H 'Authorization: Bearer <token>'Either header works: Authorization: Bearer <token>
or x-api-key: <token>. Multiple comma-separated tokens are accepted for rotation.
/v1/status reports "auth_required":true once a token is set,
without revealing it.
Write capability (licence gate)
/v1/ingest is the only mutating action, and it is gated on the instance
licence carrying the write capability. The public build ships no licence module
at all, so it is read-only by construction and /v1/ingest returns
403. Reads are never licence-gated.
Rate limiting and load shedding
A hand-rolled guard runs on both surfaces, parameterised by one deployment policy resolved once at boot:
| Mode | Per-IP token bucket | Loadavg shed | /api write-block | Concurrency cap |
|---|---|---|---|---|
public (this box) | on: 60 cap, refill 3/s, heavy op = 6 tokens | on: heavy benchmarks get 503 | on: 403 | 32 in flight |
onprem (default self-host) | off | off | off (writes allowed) | cores × 8 |
off | off | off | off | none |
When the bucket or concurrency cap trips you get 429 with a
Retry-After header. When the box is loaded, heavy benchmarks are shed with
503 + Retry-After while cheap metadata still serves. The
guard never 500s a request itself: any doubt fails open. The self-hosted default (onprem)
keeps only the concurrency cap so an operator benchmarking their own box is never throttled.
Put your bytes in, read them out.
Customer bytes land in this instance's own store, never in the shipped demo store. Everything crossing the boundary is opaque: uniform containers, packed ids, or base64 of the same. See Extension API for how to build the containers you ingest.
GET /v1/status
Instance identity, licence, writability, auth state, and store counts. An anonymous caller on an open box gets a reduced, non-identifying view (a store basename hint, no absolute path, no licensee name); once auth is on, the authenticated caller sees the full operator detail.
curl https://one.semurg.io/v1/status{"ok":true,
"api":"semurg /v1 data api",
"writable":false,
"auth_required":false,
"licence":{"state":"unlicensed build (read-only)"},
"store":{"state":"open","name":"user.bin","shards":null}}
Response captured live on the public box (read-only). A licensed
instance reports "writable":true and its licence id, expiry and caps.
POST /v1/ingest write cap required
Append one batch of packed uniform containers. Body is JSON with containers_b64,
the base64 of the packed containers. One call, one append, one crossing,
no fsync in the path.
| Field | Type | Rules |
|---|---|---|
containers_b64 | string | base64 of a whole number of containers; max 65,536 containers (4 MB) per request; send successive batches for more |
curl -X POST https://one.semurg.io/v1/ingest -H 'content-type: application/json' -d '{"containers_b64":"<base64 of packed containers>"}'{"ok":true,"containers":65536,"bytes":4194304,"elapsed_us":<elapsed>,"containers_per_s":<measured on your box>}
On the public read-only box this returns
403 {"error":"this instance is read-only", ...}. Errors:
422 for a missing or non-base64 body or a wrong-length body,
413 for a batch over 65,536 containers, 500 if the
append itself fails.
POST /v1/query
Batch read. The op field selects one of four read primitives. It is a POST only
because it carries a JSON body; it is a read, so it is never licence-gated.
| op | params | does | result |
|---|---|---|---|
fetch | ids (list, ≤ 100,000) | batch point read, one crossing | base64 of the containers, request order, zeros for absent |
resolve | id (int) | logical id to live byte offset | integer offset, or null if absent/tombstoned |
search | needle (string) | pattern scan over raw container bytes | [matches, containers_scanned, bytes_scanned] |
histogram | field_offset (0..63, default 8), buckets (1 to 4,096, default 16) | group-by fold over a qword field | [count_0, ..., count_(buckets-1)] |
curl -X POST https://one.semurg.io/v1/query -H 'content-type: application/json' -d '{"op":"fetch","ids":[0,1,2]}'{"ok":true,"op":"fetch",
"result":{"encoding":"base64","bytes":192,"containers":3,"data":"AAAA...AAAA"},
"params":{"requested":3},"elapsed_us":<elapsed>}
curl -X POST https://one.semurg.io/v1/query -H 'content-type: application/json' -d '{"op":"histogram","field_offset":8,"buckets":4}'{"ok":true,"op":"histogram","result":[0,0,0,0],"params":{"field_offset":8,"buckets":4},"elapsed_us":<elapsed>}
Responses captured live on the public box (its
/v1 store is intentionally empty, so counts read as zero). A missing
op or an unsupported one returns 422 with a hint;
an empty ids list is 422; over 100,000 ids is
413.
POST /v1/fetch_batch binary
The binary twin of /v1/query op=fetch: same primitive, same single crossing, but
no JSON or base64 wrap on the container bytes. Request body is application/octet-stream,
a packed array of little-endian u64 ids (8 bytes each). Response is application/octet-stream,
exactly one fixed-size container per id in request order, an all-zero container for an absent id. Cap
is 1,048,576 ids (8 MiB) per request; a body that is not a whole number of ids is rejected
400 (plain text, since the route otherwise returns raw bytes).
printf '\x00\x00\x00\x00\x00\x00\x00\x00' | curl -X POST https://one.semurg.io/v1/fetch_batch -H 'content-type: application/octet-stream' --data-binary @- | xxd | headVerified live: one id in returns exactly one container out.
What is not a JSON endpoint yet.
The engine does more than the four /v1/query ops expose. These capabilities run
today on the interactive faces (in-process, on the same one store) but are not yet a JSON
/v1 endpoint. They are honestly on the roadmap.
{"op":"vector"} (or any JSON vector search) on /v1.
Vector code containers can be ingested and fetched like any other container, but binary
approximate-nearest-neighbour search is only exercised through the interactive
vector face in-process, not over
HTTP. Passing op=vector to /v1/query returns
422 today (verified live).| Capability | Status over HTTP | Where it runs today |
|---|---|---|
| Vector / ANN search | roadmap no JSON op | /demo/vector, in-process |
| General graph traversal (k-hop, neighbours, shortest path) | roadmap no general JSON op | the /demo/g* faces and /api/bench/graph_teps (fixed BFS benchmark) |
| SQL / OLAP over HTTP | roadmap no JSON op | the /demo/sql* and /demo/olap faces, in-process |
| Value-flow forensic trace | roadmap no JSON op | the collapse and dataset faces, in-process |
The building blocks for all of these are the extension API on the Develop page: an extension in-process has the full engine surface (scan, histogram, traversal, vector codes) that the JSON API only partly re-exports.
Reproduce the numbers.
Every figure the demos show is measured live on this box. The benchmark API lets you run the same queries programmatically and get the same numbers, then download the public dataset and re-run to verify. Params come from the query string (GET) or a JSON body (POST); both work.
GET /api/health
Liveness, which datasets are ready, the live engine state, and the local model-serving status.
The engine block asserts the native engine is the engaged
read path (not the OS page cache).
curl https://one.semurg.io/api/health{"ok":true,"service":"semurg benchmark api",
"engine":{"mode":"native","arms":{"shard":"engaged","chain":"engaged","snn":"engaged"}},
"datasets":{"binance":"warming","btc-chain":"warming"},
"intelligence":{"backend":"local_e4b","model":"gemma4:e4b","serving":false,"detail":"native_module_absent"}}
Captured live. The datasets map and the
intelligence block are instance-dependent and reflect what is staged and warmed on
the box at the moment of the call.
GET /api/benchmarks
The catalog: every runnable benchmark key, what it measures, which datasets it accepts, and its params.
curl https://one.semurg.io/api/benchmarks| key | measures | datasets | class |
|---|---|---|---|
graph_teps | deep k-hop BFS, traversed edges/second | graph datasets (pokec, web-google, ...) | heavy |
traversals | whole-graph BFS, cold + warm TEPS | graph datasets | heavy |
reads | hot (L3) + warm (DRAM) reads/second | graph datasets | heavy |
scans | resident scan throughput (GB/s) | graph datasets | heavy |
saops | raw SNN kernel synaptic-ops/second | synthetic bit slabs | cheap |
writes | pure-append containers/second | synthetic fresh handle | write (blocked on public) |
GET POST /api/bench/:key
Run one benchmark. Read-only keys only on the public box; the write keys
(writes and friends) are refused 403 under the public
policy. An unknown key returns 404 with the available list; a warming or absent
dataset returns 503 (retry shortly); malformed params return
422.
curl 'https://one.semurg.io/api/bench/saops?iters=50000'{"benchmark":"saops",
"result":{"saops":<measured on your box>,"synops":1638400000,"saops_human":"<your figure>"},
"params":{"iters":50000},
"elapsed_s":<elapsed>,
"provenance":{"native_engaged":true,"honesty_band":"(c) measured on native silicon"}}
Response captured live. graph_teps takes
dataset, hops (1..8), seed and
span; it returns 503 while its dataset is warming.
GET /api/dashboard heavy
One-call headline snapshot: it re-runs the full heavy battery (multi-million-container writes plus BFS traversals) and is memoized for 5 seconds so a burst cannot re-run it per request. Classed heavy, so it is shed under load on the public policy.
curl https://one.semurg.io/api/dashboard{"measured_live_on":"<hostname>",
"writes":{"result":{"containers":1000000,"writes_per_s":<measured on your box>}, ...},
"writes_16shard":{...},"reads":{...},"traversals":{...},"scans":{...},
"saops":{"result":{"saops":<measured on your box>,"saops_human":"<your figure>"}, ...}}
Captured live; keys whose dataset is still warming report a
{"status":"warming"} stanza rather than a number, which is the honest state on a
freshly booted box.
Issued-token bundle delivery.
Two token-gated routes deliver an evaluator their own installer bundle and licence key:
GET /d/:token and GET /d/:token/license.key. A bad or
absent token is refused 403 (verified live). This is Semurg's own gated delivery,
not part of the data API, and it is pruned from a customer instance build: a customer box has nothing to
hand out, so these routes do not exist there.
One table for both surfaces.
| Code | Where | Meaning |
|---|---|---|
200 | all | OK. JSON body, or raw bytes for /v1/fetch_batch. |
400 | /v1/fetch_batch | body not a whole number of 8-byte ids, or over the id cap. Plain-text body. |
401 | /v1/* | token required and not presented. Only when a token is provisioned; the public box never returns this. |
403 | /v1/ingest, /api/bench/<write>, /d | read-only instance (no write cap), or public write-block, or bad download token. |
404 | /api/bench/:key | unknown benchmark key; body lists the available keys. |
413 | /v1/query, /v1/ingest | too many ids (> 100,000) or too large a batch (> 65,536 containers). |
422 | /v1/query, /v1/ingest | missing or malformed op, empty ids/needle, out-of-range histogram params, non-base64 body. |
429 | all (public) | per-IP rate limit or global concurrency cap. Carries Retry-After. |
500 | /v1/ingest | the engine append failed. |
503 | /api/* | dataset warming/absent, or heavy benchmark shed under load. Carries Retry-After where applicable. |