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
| what | limit |
|---|---|
| Publishes per key | 10 per rolling 24 hours (E_QUOTA_EXCEEDED) |
| Publishes per address | 30 per rolling hour |
| Verification requests per key | 10 per rolling 24 hours |
| Bursts | Short 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.
{ "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.
{ "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).
{ "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:
{
"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
}mineis true when the request was made with the calling key. Only then iskey_idpresent; other callers seerequester_label.- After a decision:
reviewed_at,reviewer_label,decision_noteand, when the verifier ran one,reproduction.reviewer_idis 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_tieris the page's current tier.
GET/api/v1/keys/mekey
{ "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.
{ "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).
{ "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
{ "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
| hash | what it covers |
|---|---|
| content_hash | sha256: + 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_sha256 | Hex SHA-256 of the exact request body bytes, $template included. What the CLI records in its ledger. |
| source_sha256 | Hex SHA-256 of the local file as read, before L80 stripped private fields. Client-side only. |