Every example below targets your own app-api origin — shown here as https://your-app-api-host.example, never this site's own hostname — and uses placeholder credentials. Request and response shapes are copied from the current app-api handlers, not reconstructed from memory; field names and status codes are exact.

Log in and read the current session

POST /api/v1/auth/login
Content-Type: application/json

{ "email": "you@example.com", "password": "<password>" }
200 OK
{
  "token": "<jwt>",
  "expires_at": "2026-09-08T13:00:00Z",
  "user": { "id": "...", "email": "you@example.com", "name": "...", "role": "admin", "organization_id": "..." }
}
GET /api/v1/auth/me
Authorization: Bearer <jwt>
200 OK
{ "id": "...", "email": "you@example.com", "name": "...", "role": "admin", "organization_id": "..." }

Register a Runtime

POST /api/v1/runtime/register
X-API-Key: erk_rt_v1_<lookup>_<secret>
Content-Type: application/json

{
  "installation_id": "<stable local install identifier>",
  "name": "ci-runner-1",
  "version": "0.9.8",
  "hostname": "ci-runner-1.internal",
  "os": "linux",
  "nonce": "<32 random bytes, base64url, unpadded>"
}
201 Created
{
  "runtime_id": "...",
  "name": "ci-runner-1",
  "status": "registered",
  "bundle": { "envelope": { "domain": "engineeringruntime.sync-bundle/v1", "..." }, "policy": "...", "config": "...", "public_key": "..." },
  "receipt": { "domain": "engineeringruntime.registration-receipt/v1", "nonce": "...", "origin": "...", "..." }
}

Fetch the active sync bundle — with and without changes

GET /api/v1/sync-bundles/active
X-API-Key: erk_rt_v1_<lookup>_<secret>
200 OK
ETag: "1.3-9f2c..."
{ "envelope": { "..." }, "policy": "...", "config": "...", "public_key": "..." }

Repeating the request with the ETag you were given:

GET /api/v1/sync-bundles/active
X-API-Key: erk_rt_v1_<lookup>_<secret>
If-None-Match: "1.3-9f2c..."
304 Not Modified

Publish a policy version

POST /api/v1/policies
Authorization: Bearer <jwt>
Content-Type: application/json

{ "version": "v2026090801", "name": "enterprise-policy", "yaml": "<policy YAML>" }
201 Created
{ "version": "v2026090801", "name": "enterprise-policy", "active": true, "updated_at": "...", "updated_by": "you@example.com", "description": "", "yaml": "<policy YAML>" }

Publishing the same version twice is a conflict, not a silent overwrite — versions are immutable so audit records stay resolvable against the exact policy that produced them:

409 Conflict
{ "error": "policy version v2026090801 already exists — versions are immutable so audit records stay resolvable" }

POST /api/v1/configurations follows the identical shape and the identical 409 behavior for managed Runtime configuration.

Create and replace an API key

POST /api/v1/api-keys
Authorization: Bearer <jwt>
Content-Type: application/json

{ "name": "ci-runner-1" }
201 Created
{
  "id": "...",
  "name": "ci-runner-1",
  "state": "unclaimed",
  "api_key": "erk_rt_v1_<lookup>_<secret>",
  "expires_at": "2026-09-09T12:00:00Z",
  "warning": "copy this key now; it will not be shown again"
}

The api_key field is show-once — it is never returned again by GET /api/v1/api-keys, which instead shows a truncated prefix. To rotate credentials for an already-registered Runtime without re-registering it:

POST /api/v1/api-keys/{id}/replace
Authorization: Bearer <jwt>
201 Created
{ "id": "...", "name": "ci-runner-1 replacement", "state": "pending_replacement", "api_key": "erk_rt_v1_<lookup>_<secret>", "expires_at": "2026-09-08T13:00:00Z", "warning": "copy this key now; it will not be shown again" }

The replacement key activates — and its predecessor is revoked — the moment the Runtime authenticates with it for the first time, not when it is created. See Authentication for the full lifecycle.

Read the fleet and the catalog

GET /api/v1/fleet
Authorization: Bearer <jwt>
200 OK
{ "runtimes": [ { "name": "ci-runner-1", "status": "online", "version": "0.9.8", "policy_version": "v2026090801", "last_seen": "...", "hostname": "...", "os": "linux" } ] }
GET /api/v1/providers
Authorization: Bearer <jwt>
200 OK
{ "providers": [ { "id": "github", "name": "GitHub", "status": "connected", "capabilities": 42, "operations": 12, "auth": "token", "last_used": "...", "scope": "" } ], "catalog": "...", "generated_at": "..." }
GET /api/v1/capabilities
Authorization: Bearer <jwt>
200 OK
{ "capabilities": [ { "id": "github/pr-list", "name": "List pull requests", "provider": "github", "description": "...", "path": "..." } ], "total": 1, "catalog": "...", "generated_at": "..." }
GET /api/v1/specifications
Authorization: Bearer <jwt>
200 OK
{ "specifications": [ "..." ], "runtime": "0.9.8", "generated_at": "..." }