Documentation menu

HTTP API

Everything the L80 CLI does goes through this API, so a tool of your own can too. Base URL https://launch80.com. All bodies are JSON.

Basics

Authentication

Endpoints marked key take a publish token as a bearer credential. A token comes with the Supporter role. The server stores only its SHA-256.

Authorization: Bearer L80_<keyid>_<secret>

Envelope

Every response, success or error, is one of:

{ "ok": true,  "data": … }
{ "ok": false, "error": { "code", "message", "remedy", "details"?, "request_id" } }

details is a list of { path, message } with a JSON Pointer into what you sent. A 429 carries Retry-After. Unknown paths under /api answer E_NOT_FOUND in the same envelope. Every code.

Rate limits

whatlimit
Publishes per key10 per rolling 24 hours (E_QUOTA_EXCEEDED)
Publishes per address30 per rolling hour
Verification requests per key10 per rolling 24 hours
BurstsShort per-key and per-address limits on every endpoint (E_RATE_LIMITED)

Artifacts

POST/api/v1/artifactskey

Publish a payload. The body is the payload itself, with $template naming one of the templates. Order of checks: authenticate, rate limits, quota, size cap (4 MB hard, then the template's own), parse, structural safety, schema. The exact bytes are stored.

201
{ "guid", "url", "template_id", "template_version", "trust_tier": "unverified",
  "content_hash", "byte_size", "created_at" }

GET/api/v1/artifacts?limit=50&before=<iso>key

The caller's own pages, newest first (limit 1–200). next_before is set when a full page came back; pass it as before for the next.

200
{ "key_id", "next_before",
  "artifacts": [{ "guid", "url", "template_id", "template_version", "title", "byte_size", "created_at",
                  "trust_tier", "verified_at",
                  "verification"?: { "status", "requested_at", "reviewed_at" } }] }

GET/api/v1/artifacts/{guid}public

One page as data: metadata and the stored payload (without $template).

200
{ "guid", "template_id", "template_version", "trust_tier", "verified_at", "title",
  "content_hash", "created_at", "payload" }

GET/api/v1/artifacts/{guid}/payloadpublic

The body the page was published with, exactly as received, as data: for a BetterBench page, the results.json a verifier reproduces. The bytes are spliced into the envelope unchanged, so sha256 of data equals the publisher's body_sha256, also sent as X-Body-SHA256. Pages from before bodies were stored come back re-serialized with X-Body-Verbatim: false.

Verification

The workflow is described in Get verified. Endpoints marked key accept any valid key unless noted. Every one returns a Verification:

Verification
{
  "id": "6c1e…", "artifact_guid": "9f1b…", "url": "https://launch80.com/a/9f1b…",
  "title": "Qwen3-8B on vLLM 0.9.3",
  "status": "requested", "trust_tier": "pending",
  "requested_at": "2026-10-18T12:00:00Z",
  "requester_label": "alice",
  "note": "vLLM 0.9.3, --max-model-len 32768",
  "client_checks": [{ "name": "cold_cache", "status": "pass", "detail": "…" }],
  "server_checks": [{ "name": "cold_cache", "status": "pass", "detail": "…" }],
  "mine": false
}
  • mine is true when the request was made with the calling key. Only then is key_id present; other callers see requester_label.
  • After a decision: reviewed_at, reviewer_label, decision_note and, when the verifier ran one, reproduction. reviewer_id is shown only to that reviewer. A rejection's note is shown to the publisher, the reviewer and other verifiers.
  • status: requested, in_review, verified, rejected or withdrawn. trust_tier is the page's current tier.

GET/api/v1/keys/mekey

200
{ "key_id": "alice", "label": "alice", "is_verifier": false }

POST/api/v1/artifacts/{guid}/verificationowner key

Ask for a page to be verified. Only the key that published it (E_UNAUTHORIZED), only bench.betterbench.v1 and scan.pwdet.v1 pages (E_VERIFY_INELIGIBLE), one open request per page (E_VERIFY_EXISTS, 409). All fields are optional.

body
{ "note": "how to reproduce, ≤ 2000 chars",
  "client_checks": [{ "name", "status": "pass|warn|fail", "detail" }],
  "source_sha256": "hex", "body_sha256": "hex", "content_hash": "as returned at publish",
  "l80_version": "1.0.0" }

The server runs the checks on the stored payload. If any fails, the attempt is recorded and closed at once (status rejected, no reviewer), the page stays unverified, and the answer is E_VERIFY_INELIGIBLE (422) with one details entry per failed check. Otherwise the page turns pending and the response is the Verification (201).

GET/api/v1/artifacts/{guid}/verificationkey

The page's latest request, or E_NOT_FOUND.

GET/api/v1/verifications?scope=queue|mine&status=key

An array of Verification, oldest first, at most 200. scope=queue (the default) is requests made by other keys, never the caller's own; scope=mine is the caller's own. status filters; omitted or all means every status.

GET/api/v1/verifications/{id}key

One request, with mine set for the caller.

POST/api/v1/verifications/{id}/decisionverifier key

Verifier keys only (E_NOT_VERIFIER, 403), never on the caller's own request (E_OWN_REQUEST, 403). The request must be open, else E_VERIFY_CLOSED (409). A rejection needs a note. reproduction is optional and stored as sent, except on a pwdet scan (run_class scan): approving one needs pwdet's comparison with template: "scan.pwdet.v1", a scan object and within_tolerance: true, else E_REPRODUCTION_REQUIRED (422).

body
{ "decision": "verified" | "rejected",
  "note": "required for rejected",
  "reproduction": {
    "betterbench_version": "0.6.1", "model": "Qwen3-8B",
    "notes": { "engine": "vLLM 0.9.3", "hardware": "1x RTX 4090" },
    "runs_per_category": 20, "phases": ["decode", "prefill", "concurrency"],
    "metrics": [{ "label": "Combined decode", "unit": "t/s", "published": 142.4, "reproduced": 139.8,
                  "delta_pct": -1.8, "tolerance_pct": 10, "within": true }],
    "within_tolerance": true, "results_sha256": "hex", "ran_at": "RFC 3339", "l80_version": "1.0.0" } }

Sets the page to verified (with verified_at) or rejected and returns the updated Verification.

Service

GET/api/v1/healthpublic

200
{ "service": "launch80-artifacts", "version": "v1",
  "templates": ["bench.report.v1", "bench.betterbench.v1", "generic.report.v1", "scan.pwdet.v1"],
  "verification": true, "betterbench_max_supported": "0.6.1" }

GET/api/v1/templatespublic

[{ id, version, label, schema_url }]

GET/api/v1/templates/{id}/schemapublic

The template's JSON Schema (draft 2020-12).

Hashes

hashwhat it covers
content_hashsha256: + hex SHA-256 of template_id, a newline, then the payload without $template serialized with object keys sorted at every level and no whitespace. Two payloads that differ only in key order hash the same.
body_sha256Hex SHA-256 of the exact request body bytes, $template included. What the CLI records in its ledger.
source_sha256Hex SHA-256 of the local file as read, before L80 stripped private fields. Client-side only.