Reference
API reference
Base URL https://api.verify.tensorcash.boblabs.eu. All replies are JSON.
The machine-readable version is openapi.yaml.
Conventions
Authentication. Every endpoint except GET /healthz takes a key from the
portal: Authorization: Bearer tvk_…. Portal keys carry verify:invoke and
verify:full; verify:share-mode is added on request for share
verification. Missing or unknown key: 401. Missing scope: 403.
Network. Mainnet only. /v1/… and /net/tensor-main/v1/… are the same
routes.
Submit bodies. The raw bytes of a ValidationRequest FlatBuffer whose
request union is a BlockValidation (schema
shared-utils/fb-schemas/validation.fbs in the TensorCash repository), sent
as Content-Type: application/octet-stream, at most 50 MB. hash_id in every
reply is the request's hash as lowercase hex in display order, the form
explorers show.
Reply formats. Without extra headers, replies are flat objects keyed by
hash_id (the plain format, what nodes before 1.2.3 speak and what share
verification uses). With X-Verify-Result-Contract: scoped/1 the reply is
keyed by its scope (network, operation, block, parent, height, ruleset,
model, detector policy) and its scope_key, the SHA-256 of the scope's
canonical JSON; the request must then carry height and ruleset. Use the
scoped format for new block clients.
Headers on submit routes.
| Header | Value | Notes |
|---|---|---|
Content-Type |
application/octet-stream |
Required. |
Accept |
application/json or absent |
A value that excludes JSON is 406. |
X-Verify-Result-Contract |
scoped/1 |
Scoped reply format. Blocks only. |
X-Target-Override |
64 hex characters | Share target. Full and Quick-Smell submit routes and their status routes; needs verify:share-mode. Not combinable with the scoped format. |
Submit (asynchronous)
Return at once; poll for the result. Scope verify:invoke; Full also needs
verify:full.
| Route | Check | Where |
|---|---|---|
POST /v1/verify/quick/request/submit |
Quick: proof-of-work, header, parameter bounds, token sequence | CPU, seconds |
POST /v1/verify/quick-smell/request/submit |
Quick plus a statistical test of the sampled tokens | CPU, seconds |
POST /v1/verify/full/request/submit |
Quick plus a replay of the model and a comparison of the logits | GPU, about 10 s |
Scoped format, new request: 202, header Retry-After (5 for Quick, 30
for Full).
{
"status": "pending", "state": "queued", "verdict": null, "final": false,
"hash_id": "6f1c…e2a9", "verification_type": "quick",
"scope": {
"scope_version": 1, "network": "tensor-main", "operation": "Quick",
"subject": {"kind": "block", "hash_id": "6f1c…e2a9", "pow_blob_hash": "a3…17", "object_ref": null},
"parent": "00000…4b2c", "height": 29301, "ruleset": 4,
"model_identifier": "Qwen/Qwen3-8B@9c925d64d72725edaf899c6cb9c377fd0709d9c5",
"detector": {"entry_digest": null, "state": "na"}, "target_mode": "block"
},
"scope_key": "5b0d…c7e1",
"status_ref": "/v1/verify/result/5b0d…c7e1",
"retry_after_seconds": 5,
"request_generation": null
}
state is queued, running or needs_reverify (then an outcome object
says why no verdict was produced). operation is Quick, Quick_Smell or
Full.
Scoped format, result known: 200, the same body as
GET /v1/verify/result/{scope_key}.
Plain format, new request: 202
{"status": "pending", "hash_id": "6f1c…e2a9", "verification_type": "full", "state": "accepted"}
(Quick-Smell answers {"status": "pending", "hash_id": "…"}.) Poll Quick with
the batch route and verification_type: "quick"; Quick-Smell on
GET /v1/verify/status/quick-smell/{hash_id}; Full on
GET /v1/verify/status/full/{hash_id} or the batch route.
Plain format, result known: 200
{"status": "Full_Green", "hash_id": "6f1c…e2a9", "pow_blob_hash": "a3…17", "elapsed_ms": 10400, "cached": true}
With X-Target-Override (shares): the proof-of-work check uses the header
value instead of the block target. A share above the target is answered 200
at once with "status": "Full_Red" (or Quick_Fail on Quick-Smell),
"target_override_applied": true, "target_override_met": false and
computed_header_hash. Otherwise the flow is as above and the result carries
"target_override_applied": true and computed_header_hash. See
Verifying shares.
503 with Retry-After: the request could not be taken now; retry.
Submit (synchronous)
Hold the connection until the verdict exists. Plain format only. Kept for
older clients; prefer the asynchronous routes. The connection is closed after
about five minutes; the backend answers 504 when its own budget runs out.
| Route | Scopes | Statuses |
|---|---|---|
POST /v1/verify/quick |
verify:invoke |
Quick_OK, Quick_Fail |
POST /v1/verify/quick-smell |
verify:invoke |
Quick_*_Smell_* |
POST /v1/verify/full |
verify:invoke, verify:full |
Full_Green, Full_Amber, Full_Red |
POST /v1/verify/full/request |
verify:full |
as /v1/verify/full, without the options below |
Options on the first three: ?details=true (or X-Verify-Details: true)
asks for failure details; Content-Encoding: gzip is accepted (decompressed
body under 50 MB). A known verdict is returned at once with "cached": true.
Results
Status reads never start work and are not counted against submit limits.
GET /v1/verify/result/{scope_key}
Scoped lookup. scope_key is 64 lowercase hex characters. verify:invoke,
plus verify:full for a Full scope.
| Reply | Body |
|---|---|
200 |
The result (below). |
202, Retry-After |
Pending, same shape as the submit 202. |
404 |
{"status": "unknown", "scope_key": "…"}: no result and no job. Submit again. |
{
"status": "Full_Green", "verdict": "Full_Green", "final": true,
"hash_id": "6f1c…e2a9", "verification_type": "full",
"scope": {"…": "operation Full"}, "scope_key": "9a4e…02bd", "request_generation": null,
"policy": {"ruleset": 4, "model_identifier": "Qwen/Qwen3-8B@9c92…", "detector": {"entry_digest": null, "state": "na"}},
"execution": {"executed_at": 1791325410.8, "elapsed_ms": 10400, "coalesced": false, "attempt": 1, "route": "scoped", "cached": true, "assurance": "unsampled"}
}
POST /v1/verify/status/batch
Several results in one call, Content-Type: application/json, body at most
1 MB. verify:invoke; verify:full if any item is full.
{
"items": [
{"hash_id": "6f1c…e2a9", "verification_type": "quick"},
{"hash_id": "6f1c…e2a9", "verification_type": "full", "scope_key": "9a4e…02bd"}
],
"wait_ms": 2000
}
verification_type is quick, quick_smell, full or pow. An item with
scope_key is answered only by a result of exactly that scope; one without
is a plain lookup by hash. wait_ms (0 to 10000) holds the call until the
first item completes. Share results are not visible here.
{
"completed": [
{"status": "Quick_OK", "hash_id": "6f1c…e2a9", "verification_type": "quick", "elapsed_ms": 6410, "cached": true, "stored": true}
],
"still_pending": [
{"hash_id": "6f1c…e2a9", "verification_type": "full", "scope_key": "9a4e…02bd", "scope": {"…": "…"}, "state": "running", "final": false}
],
"waited_ms": 2000,
"server_ts": 1791325399.4
}
An item that is neither completed nor known appears in neither list. A plain
quick item is also satisfied by a stored Full_Green for the same block
(reported as Quick_OK).
GET /v1/verify/status/full/{hash_id}
A Full result by hash. verify:invoke and verify:full. 200 with the
plain result, 202 with {"status": "pending", "state": "…", …}, or 404
{"detail": "No cached result found"}. With X-Target-Override it looks up
the share result for that target. With ?scope_key=… and
X-Verify-Result-Contract: scoped/1 it is the scoped lookup restricted to
Full.
GET /v1/verify/status/quick-smell/{hash_id}
The same for Quick-Smell. verify:invoke.
GET /v1/verify/status/{hash_id}
Any-type lookup by hash: Full (with verify:full), then Quick-Smell, then
other kinds; it does not find plain Quick results. verify:invoke.
Service
GET /v1/verifier/capabilities
What the service is serving right now, as the tensorcash.verifier.capabilities/1
document nodes from 1.2.3 read. Check serving[].operations before relying
on a lane: only "state": "ready" is served.
{
"schema": "tensorcash.verifier.capabilities/1",
"service_version": "1.2.3",
"network": "tensor-main",
"rulesets": [0, 1, 2, 3, 4],
"serving": [
{
"network": "tensor-main",
"model_identifier": "Qwen/Qwen3-8B@9c925d64d72725edaf899c6cb9c377fd0709d9c5",
"operations": {
"Quick": {"state": "ready", "rulesets": [0, 1, 2, 3, 4]},
"Quick_Smell": {"state": "unavailable", "reason": "smell_stats_missing"},
"Full": {"state": "ready", "rulesets": [2, 3, 4]}
}
}
],
"…": "…"
}
GET /v1/corrections
Verdicts that were changed after they were published, oldest first.
?since=<ISO 8601> (default: the whole 7-day window), ?limit= (1 to 5000,
default 1000).
{
"corrections": [
{
"hash_id": "6f1c…e2a9", "network": "tensor-main", "verification_type": "full",
"request_context": "h=29301|rs=4|d=1234", "target_mode": "block", "pow_blob_hash": "a3…17",
"original_status": "Full_Green", "corrected_status": "Full_Red", "corrected_at": 1791330000.0
}
],
"since": "2026-09-29T10:00:00+00:00", "retention_days": 7.0, "server_ts": 1791334800.2
}
A correction names one exact verification (request_context, target_mode
block or share); later reads of that result return the corrected status.
GET /healthz
No key needed. {"status": "ok"} when the API is up.
Error shapes
| Shape | Where |
|---|---|
{"detail": "…"} |
Most 4xx and 5xx |
{"error": "…", "detail": "…"} / {"error": "…", "retry_after": n} |
401 and 429 |
{"status": "refused" \| "rejected" \| "pending", "verdict": null, …} |
Scoped format refusals, rejections and no-verdict outcomes |
Every error with its cause and action: Limits and errors.
Updated 2026-10-07