`POST /api/runners/enroll` — mint a single-use enrollment token. A system admin may always mint; an org/workspace owner may mint only when the `runner_onboarding_by_owner` switch is on AND the scope is a non-empty `edge` set of workspaces they own (ADR 0046 Phase 0, decision #9). The runner later exchanges the token for a scoped JWT. The plaintext is returned once; the audit payload carries no token material.
const url = 'https://example.com/api/runners/enroll';const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"allowed_labels":["example"],"allowed_workspace_ids":["2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"],"class":"example","isolation":"example","offer_id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","owner_scope":"example","tier":"example","ttl_secs":1}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/api/runners/enroll \ --header 'Content-Type: application/json' \ --data '{ "allowed_labels": [ "example" ], "allowed_workspace_ids": [ "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" ], "class": "example", "isolation": "example", "offer_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "owner_scope": "example", "tier": "example", "ttl_secs": 1 }'Request Bodyrequired
Section titled “Request Bodyrequired”Body to mint a single-use enrollment token. class/tier/isolation are
validated by the enrollment service; the optional scope narrows the eventual
grant.
object
Optional finer label restriction (a task’s required labels must be a subset).
Explicit workspace allow-list. Empty + trusted = wildcard; empty + edge
= fail-closed (dispatched nothing until granted a workspace).
trusted | edge. An edge enrollment fail-closes to no workspace.
microvm | container — the isolation the runner provides.
#521 — a marketplace runner offer this enrollment is backed by. When set, the mint is authorized by an active purchase + install for the offer in the single buyer workspace (FAIL-CLOSED, EDGE-ONLY) and the minted grant is metering-linked.
#716 — the EXPLICIT enrollment scope the wizard chose, decoupled from the mount:
"instance" mints an instance runner (org_id = None, system-admin only) and
"org" mints an org-owned runner via the owner derivation. Absent keeps the
byte-identical default (a system admin → instance; a non-admin → the org owner
path). A non-admin may NEVER request "instance" (fail-closed Forbidden).
wasm | docker | both — the execution tier the runner may serve.
Enrollment-token lifetime in seconds (clamped to 60..=3600 by the service).
Examplegenerated
{ "allowed_labels": [ "example" ], "allowed_workspace_ids": [ "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" ], "class": "example", "isolation": "example", "offer_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "owner_scope": "example", "tier": "example", "ttl_secs": 1}Responses
Section titled “Responses”Enrollment token minted (returned once)
The minted enrollment token, returned once (never retrievable again).
object
The single-use plaintext enrollment token — shown exactly once.
How long the token is valid (seconds).
A copy-paste hint for the operator on how to use the token.
Examplegenerated
{ "enrollment_token": "example", "expires_in_secs": 1, "install_hint": "example"}Invalid class/tier/isolation
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Authentication required
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Permission denied
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Structured server error
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}