JSON over HTTPS. Public reads need nothing; writes and personal reads take a bearer token made on /me:
Authorization: Bearer liberas_…
Errors are { "error": { "code": "…", "message": "…" } } with an HTTP status. Messages produced by verification also come as i18n: { code, params } for rendering in another language. The liberas CLI (@pairful/liberas, source in scripts/liberas.mts) is a thin client for everything here.
Specs
GET /api/specs?status=open&product=P
{ "specs": [ {
"id": "calc-div", "product": "calc", "title": "Divide", "summary": "Returns a / b as an f64. …",
"shares": 80, "export": "liberas:calc/operation@0.1.0", "dependsOn": [],
"execution": "remote", "maxFuelRatio": null, "status": "open", "broken": false,
"fulfilledBy": null, "createdAt": "2026-10-21T…"
} ] }fulfilledBy is the Liberas ID that earned the bounty, or null while open. broken is true when a contributed case showed the current component disagreeing with the spec: the next component that passes every case replaces it (see cases below). execution is local (ships in the product) or remote (runs on Liberas; called through /api/run). canonicalizer, when set, names a spec of the same product whose reference maps every output before it is compared (ADR 0031): an encoder is verified through its decoder, so any valid encoding passes. fuelPerCall is the instruction budget of one remote call (null: 50 million).
GET /api/specs/{id}
{
"spec": { …as above… },
"wit": { "calc-div.wit": "package root:component;\n\nworld root {\n export liberas:calc/operation@0.1.0;\n}\npackage liberas:calc@0.1.0 {\n interface operation { … }\n}" },
"interface": {
"exportName": "operation",
"functions": [ { "name": "apply", "params": [ { "name": "a", "type": "f64" }, { "name": "b", "type": "f64" } ], "results": [ "result<f64, string>" ] } ],
"types": [ { "name": "image", "kind": "record", "definition": "record { width: u32, height: u32, pixels: list<u8> }" } ]
},
"cases": [ { "args": { "a": 1, "b": 0 } } ],
"submit": { "method": "POST", "url": "/api/specs/calc-div/submissions", "contentType": "application/wasm", "maxBytes": 16777216 },
"build": [ "…how to build, step by step…" ]
}The WIT is one file: the world the reference was built against (root), with every package it uses nested inside. Build against it with world: "root".
POST /api/specs/{id}/submissions — token
Body: the .wasm bytes (Content-Type: application/wasm, ≤ 16 MB). Answer 201 when it passed, 200 when it failed or was rejected (the request itself was fine), 409 if the spec is closed.
{
"status": "passed", // passed | failed | rejected
"message": "Passed all 64 cases · fuel 0.83× the reference",
"i18n": { "code": "verdict.passed", "params": { "cases": 64, "ratio": "0.83" } },
"componentHash": "b010…",
"issuanceSeq": 4, // ledger entry of the shares earned; null if none
"referenceFuel": 8361156, "candidateFuel": 6959164
}Verdict codes: passed, replaces (passed and replaces the current component), fixes (passed where the current component fails a case: replaces it for 25% of the shares), notBetter (passed, no shares), mismatch, expectedOk, expectedErr, fuelExhausted, trap, tooSlow, noEvidence; rejections: notBinary, invalidComponent, hasImports, missingImport, missingExport, missingFunction, signatureMismatch, unsupportedInterface, sameAsReference, sameAsCurrent, tooLarge.
POST /api/specs/{id}/cases — token
A test vector the spec's current component gets wrong (contributed cases). Body: { "fn": "apply"?, "args": { "a": 1, "b": 0 } } (fn when the interface has several functions). The reference and the current component run on it; 201 when they differ (the case joins the spec's vectors and earns 10% of its shares), 200 otherwise, 409 while the spec has no component (spec_not_built).
{ "status": "passed", "message": "Case apply(a=1.0, b=0.0): the spec says error \"division by zero\", but the current component answers \"Infinity\". Added to the spec's vectors: 8 shares.",
"i18n": { "code": "verdict.caseAccepted", "params": { … } }, "caseHash": "…", "issuanceSeq": 7 }Codes: caseAccepted, caseAcceptedOwn (your own component: accepted, no shares), caseRedundant (the current component answers it correctly), caseKnown (already a vector), caseReferenceFails (the reference itself fails there), caseBadInput.
GET /api/specs/{id}/cases
{ "cases": [ { "args": {…}, "fn"?: …, "by": "bob", "caseHash", "issuanceSeq", "createdAt" } ] } — the cases contributors added, oldest first.
Proposals
POST /api/proposals — token
{ "id": "calc-pow", "product": "calc", "title": "Power", "summary": "…exact behavior…",
"suggestedShares": 60, "cases": [ { "args": { "a": 2, "b": 10 } } ],
"component": "<base64 of the .wasm>" }The component is run before the proposal is accepted (422 with the reason otherwise: it does not run, imports a spec that does not exist, or generated inputs rarely succeed and cases is empty). Answer 201 { "id", "specId", "status": "pending" }.
An import may name a proposal that is still pending in the same product: propose a subtree leaves first, then the spec that imports them. The leaves are linked by their proposers' components until approved; approval goes leaves first too (errors.dependencyPending otherwise).
GET /api/me/proposals — token
Your proposals with status (pending | approved | rejected), shares once approved, note if rejected.
Admins: POST /api/specs, PUT /api/products/{id}
Register a spec or save a product with an admin's token, as the admin pages do (pnpm demo:image builds Liberas Image this way). POST /api/specs takes { id, product, title, summary, shares, execution, maxFuelRatio, cases: [ … ], canonicalizer, fuelPerCall, reference: "<base64 of the reference .wasm>" } → 201 { id, status: "open" }, 422 with the verdict when the reference or the canonicalizer is unusable. PUT /api/products/{id} takes { name, description, price, currency, roots: [ … ], calls, onSale } (calls: remote calls one purchase allows, null for unlimited) and returns the product; GET /api/products/{id} is public.
Admins: GET /api/proposals/pending, POST /api/proposals/{id}/decision
Decision body: { "decision": "approve", "shares": 40, "execution": "local" } or { "decision": "reject", "note": "why" }.
You
GET /api/me—{ liberasId, githubLogin, agreement }.GET /api/me/submissions— newest first:spec,status,message,componentHash,issuanceSeq,createdAt.
Products and execution
GET /api/products/{id}/manifest— the product's current components:spec,sha256,execution,dependsOn,status(pending|confirmed), plusroots(the specs the product's host calls; the components are their transitive closure, or every spec when empty),openSpecsandledgerHead.GET /api/components/{sha256}— a component that ships in a product (localonly;remoteones never leave Liberas).POST /api/run/{spec}—{ "key": "<license key>", "input": { …parameters by name… }, "fn": "apply"? }. Runs the spec's current component on Liberas with its dependencies linked.{ "ok": true, "output": …, "fuel": N, "deps": { "calc-add": { "calls": 1, "fuel": 42 } }, "component": "…" }, or{ "ok": false, "error": … }for the component's own error.403without a valid license for the product,404until the spec and its dependencies are built,429past the per-license rate limit,402 quota_exhaustedonce a license has made every call its product sold (calls). The call may burn up to the spec'sfuelPerCall.POST /api/licenses/validate—{ key, machineId }→ activation status.GET /api/orders/{id}?t=<secret from the order link>—{ id, status: "pending" | "paid" | "refunded", licensed }; the order page polls it after checkout.POST /api/payments/paddleis Paddle's notification destination (ADR 0033), not for clients.POST /api/products/{id}/trial— token. The product's free pack (ADR 0032):{ "product", "order", "key", "calls" }, an API key good fortrialCallsremote calls, once per account.201the first time,200with the same key after,400 trial_unavailablewhen the product has none.GET /api/products/{id}showstrialCallsandcalls. The CLI:liberas trial imageprints the key.
JSON values
bool, numbers and strings as themselves (64-bit integers beyond 2⁵³ also as decimal strings); floats NaN, Infinity, -Infinity as those strings; char as a one-character string; list and tuple as arrays, except list<u8>, which comes back as a base64 string and is accepted either way (an image is megabytes); record as an object; enum as its name; flags as an array of names; option as null or the value; variant as { "tag": name, "val": v }; result as { "ok": v } or { "err": e }. A function whose only result is a result<T, E> is unwrapped into ok/error by /api/run.
Ledger
GET /ledger/head— public:{ entries, head }, the hash chain's head.GET /api/ledger/export— token: the whole ledger as JSON Lines.liberas auditre-verifies it.