Skip to content
Select themeSelect language

`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.

POST
/api/runners/enroll
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 }'
Media typeapplication/json

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
allowed_labels

Optional finer label restriction (a task’s required labels must be a subset).

Array<string>
allowed_workspace_ids

Explicit workspace allow-list. Empty + trusted = wildcard; empty + edge = fail-closed (dispatched nothing until granted a workspace).

Array<string>
class
required

trusted | edge. An edge enrollment fail-closes to no workspace.

string
isolation
required

microvm | container — the isolation the runner provides.

string
offer_id

#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.

string | null format: uuid
owner_scope

#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).

string | null
tier
required

wasm | docker | both — the execution tier the runner may serve.

string
ttl_secs

Enrollment-token lifetime in seconds (clamped to 60..=3600 by the service).

integer | null format: int64
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
}

Enrollment token minted (returned once)

Media typeapplication/json

The minted enrollment token, returned once (never retrievable again).

object
enrollment_token
required

The single-use plaintext enrollment token — shown exactly once.

string
expires_in_secs
required

How long the token is valid (seconds).

integer format: int64
install_hint
required

A copy-paste hint for the operator on how to use the token.

string
Examplegenerated
{
"enrollment_token": "example",
"expires_in_secs": 1,
"install_hint": "example"
}

Invalid class/tier/isolation

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Authentication required

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Permission denied

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Structured server error

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}