Skip to content

HTTP API reference

Everything the dashboard shows and the cloud() plugin pushes rides plain HTTP against the platform — the wire is the SDK. This page enumerates that surface: every endpoint, who may call it, its parameters and defaults, and the errors a scripter will actually hit. The machine wire’s semantics (cache artifact format, stream framing, distribution messages) live in Wire protocol; this page is the route map.

All requests and responses are JSON unless noted. The base URL is your deployment’s origin (the VX_CLOUD_BASE_URL the server was booted with).

ClassHowUsed by
Anonymousnothing/health, GET /v1/meta, POST /v1/auth/register, POST /v1/auth/login
Sessionthe HttpOnly cookie set by login/register; every mutation must also send the header x-vx-csrf: 1 or it 403sthe dashboard, anyone scripting with a cookie
Machine tokenAuthorization: Bearer vxc_… (minted under Admin → Tokens)CI pushes, cache wire, agents, MCP
Eithersession or tokenanalytics reads, streams

Notes:

  • The cache wire, /v1/cache/batch, agent WS, and dist channels are machine-token-only — a session cookie is answered 403 there.
  • Ingest writes require a token (ci token required 403 for a session): history is pushed by machines, read by people.
  • ?token=<bearer> in the query string is accepted only on the WS/SSE endpoints (/v1/agents, /events, /stream), where browsers can’t set headers. Everywhere else the header is required.
  • /version intentionally returns 404 — GET /v1/meta is the identity probe.

Every data route is clamped to one (org, workspace):

  • Token: the org is derived server-side from the token row (never a client claim). A workspace-scoped token is pinned to its workspace; an org-wide token resolves ?ws=<workspace uuid>, else the org’s most-recently-seen workspace.
  • Session: the org is ?org=<uuid> when given, else the sole membership — a principal spanning several orgs without ?org= gets a 400 (org query param required). The workspace is ?ws=, else the org’s most-recent. Membership below viewer — and any org you can’t see — answers 404, not 403 (existence is not leaked).
  • An unknown ?ws= is a 404. An instance admin may read any org (the operator superuser).
RouteAuthReturns
GET /healthanonymousliveness { ok: true }
GET /v1/metaanonymousidentity + capability flags: auth: 'account', artifacts, cacheWire: 2, trustTiers, workspace count
RouteAuthBody / behavior
POST /v1/auth/registeranonymous{ email, password (≥8), displayName?, invite? }. The first registration becomes the instance admin (and gets a Default org as owner); after that signup is closed unless VX_CLOUD_OPEN_SIGNUP — an invite token is required (403 without). Sets the session cookie. 409 on a duplicate email.
POST /v1/auth/loginanonymous{ email, password }. Throttled per-email and per-IP+email with backoff; every attempt runs one argon2 verify (no user-enumeration timing oracle). Rotates the session id.
POST /v1/auth/logoutsession + CSRFDestroys the session server-side, clears the cookie.
GET /v1/auth/mesession or tokenThe principal: userId, email, displayName, instanceAdmin, org memberships + roles.
PATCH /v1/auth/mesession + CSRF{ displayName } (≤200 chars) — the only self-service profile field; email is the immutable login identity.
POST /v1/auth/passwordsession + CSRF{ currentPassword, newPassword (≥8) } — verifies the current password before re-hashing.
POST /v1/auth/invites/acceptsession + CSRF{ invite } — joins an EXISTING signed-in user to the invite’s org. The claim is atomic and single-use: of N concurrent accepts exactly one wins (the rest 403); 409 if already a member. A NEW user instead passes invite to register.

Invites expire after 7 days and are single-use. Sessions renew on use (30-day sliding window); cookies are Secure when VX_CLOUD_BASE_URL is https.

All admin routes take a session or an admin-kind token; non-GET methods with a session require the x-vx-csrf: 1 header. The role column is the minimum org role.

RouteRoleBehavior
GET /v1/admin/orgs(member of any)Your orgs, with roles.
POST /v1/admin/orgsinstance admin unless VX_CLOUD_OPEN_ORG_CREATE{ slug, name? } — 409 on a taken slug.
GET /v1/admin/orgs/:idviewerThe org + your role.
PATCH /v1/admin/orgs/:idadmin{ name?, slug? } (slug [a-z0-9-]{1,64}).
GET /v1/admin/orgs/:id/membersviewerMembers + roles.
PATCH /v1/admin/orgs/:id/members/:userIdadmin{ role }. Managing an owner (either direction) requires org owner; the last owner can never be removed or demoted (applies to instance admins too).
DELETE /v1/admin/orgs/:id/members/:userIdadminRemove the membership (same owner guards).
POST /v1/admin/orgs/:id/invitesadmin{ role? (default member) } — inviting an owner requires owner. Returns the invite token + a ready …/register?invite= URL (7-day expiry).
GET /v1/admin/orgs/:id/tokensadminList API tokens (metadata only — secrets are never re-shown).
POST /v1/admin/orgs/:id/tokensadmin{ name, tier: 'trusted'|'untrusted', kind?: 'ci'|'admin' (default ci), workspaceId?, expiresAt? }. The plaintext vxc_ secret exists exactly once: in this response. The trust tier is immutable for the token’s life.
DELETE /v1/admin/orgs/:id/tokens/:tokenIdadminRevoke — takes effect immediately (the in-process auth memo is cleared).
GET /v1/admin/orgs/:id/workspacesviewerWorkspaces.
POST /v1/admin/orgs/:id/workspacesadmin{ slug, name? } — 409 on a taken slug.
PATCH /v1/admin/orgs/:id/workspaces/:wsIdadmin{ name?, slug? } (slug [a-z0-9-]{1,64}) — 409 on a taken slug, 400 when neither field is given. Most workspaces are auto-provisioned on the first CI push and named by the client; the rename sticks, because later pushes never rewrite the name.
DELETE /v1/admin/orgs/:id/workspaces/:wsIdadminDelete the workspace and its entire history — invocations, task runs, task logs, output fingerprints, projects, repos and any workspace-scoped API token. Requires { confirm } echoing the workspace’s slug (or name); a mismatch is a 400 naming the slug. Not reversible. Once the rows are gone the workspace’s cached artifacts are swept from object storage — its whole org/<orgId>/ws/<wsId>/ scope prefix, both trust tiers and every per-PR sub-scope; the shared _org scope an org-wide token writes is never touched. The sweep is best-effort and runs after the response, so it can never stall or fail the delete: an unreachable bucket just leaves the bytes behind (unreachable, as they were before this existed) and the server logs it.

Cross-org access answers 404. There is no invite-list endpoint — invites are create-only surfaces.

Session (viewer+) or token; workspace-clamped per Tenancy resolution. Malformed numeric params are ignored (the default applies); a malformed percent-encoding in a path segment is a 400. Limits shown as default/max where the code clamps.

RouteParamsReturns
/v1/workspacesThe org’s workspaces.
/v1/runsproject, task, runId, hash, limitTask-level run rows. hash filters to one cache key server-side — the cache-entry provenance page needs every run that used a key, not just those inside the most recent page.
/v1/runs/:idOne full run (all task rows) or 404.
/v1/runs/:id/logs/:taskIdThe task’s captured log — source: 'executed' for a direct row, source: 'cache' (+ refRunId) when resolved through the producing run’s cache hash; 404 when nothing was captured.
/v1/invocationsbranch, ci (1/true), tagKey, tagValue, limitRun-header rows (command, branch, commit, CI, tags, counts).
/v1/invocations/:idOne invocation header or 404.
/v1/compare/:runIdPer-task diff of the run vs the immediately-previous invocation.
RouteParamsReturns
/v1/cache/statswindowDays (default 1)Windowed hit/run counters.
/v1/cache/hit-splitLocal-vs-remote hit split.
/v1/cache/breakdownlimit (20)Per-project cache totals.
/v1/cache/savingsTime-saved estimate (24 h + all-time).
/v1/cache/entriesCache-entry inventory.
/v1/cache/prunableminAgeDays (7)Entries eligible for pruning.
RouteParamsReturns
/v1/trends/runsbucket (hour|day, default hour), from, to, projectBucketed runs/failures/hits series (span clamped server-side).
/v1/trends/tasksproject required (400), bucket (default day), from, to, limit (≤50 tasks)Per-task duration series for the project’s heaviest tasks.
/v1/trends/heatmapdays (30)7×24 UTC build-activity grid.
/v1/trends/storagedays (30)Storage growth series.
/v1/trends/parallelismlimit (50)Per-run parallelism factors.
/v1/analysiswindow (days), minRuns, limit, project, taskPeriod-over-period comparison: this window vs the prior one + the biggest duration movers.
/v1/flakinesslimit (25), or project + task for a point lookupFlakiest tasks — retry-confirmed flakes ranked above inferred ones. Passing the pair narrows to that one task, so the task-detail badge never depends on the task ranking inside a top-N page.
/v1/flake-trendproject + task required (400), sinceDays (90)Per-day flaky-episode series for one task (retried successes + failures whose key also passed), with first/last seen — feeds the task-detail Flakiness-trend card.
/v1/stabilityproject + task required (400), sinceDays (90), limit (20)Same-cache-key duration spread — how repeatable the computation is across executions of IDENTICAL inputs. Distinct from regressions (which compare across different keys): this is the task’s margin of error, and the floor under any cross-key claim.
/v1/stability/leastsinceDays (30), limit (8), minRuns (3)The least repeatable tasks in the workspace, worst same-key spread first — an unstable task makes every duration comparison involving it unreliable.
/v1/regressionssinceDays, minBranches, limitTasks that started failing across ≥N branches and have a prior success.
/v1/branch-failuresproject required (400), sinceDays, limitPer task: the branch where the failure was first noticed, first commit, and every failing branch.
/v1/bottlenecksdays (14), limit (15)Aggregate critical-path bottlenecks.
/v1/top-taskslimit (10)Top time-burners.
/v1/failureslimit (25)Recent failed tasks.
/v1/notificationslimit (20)Recent broken invocations (the dashboard bell’s feed).
/v1/projects/rankproject required (400), top (8)Where one project ranks against EVERY other on failure rate, avg exec and hit rate — ranked with window functions server-side, so both the rank and the total are true at any workspace size rather than computed within a fetched page.
/v1/projectslimit (100), search, project (repeatable)Per-project rollups plus total, the workspace’s TRUE project count. search is a case-insensitive substring match on the project name; project is an exact-name point lookup. Both narrow server-side, so the dashboard’s filter box reaches a project past the page.
/v1/historyproject, task, search, limitPer-task lifetime aggregates. search is a case-insensitive substring match on project#task, so one box matches orders, build and orders#build alike — narrowed server-side, since the result is a page.
/v1/hermeticitylimit (50/500)Cross-machine output-fingerprint divergences (--verify=fingerprint data).
RouteReturns
/v1/tasks/:taskIdTask detail (percentiles, durations, flakiness) or 404.
/v1/explain/:taskIdThe task’s cache-key composition.
/v1/why/:runIdBatched: every executed task’s re-run verdict (first run / inputs changed / ran uncached) in one round-trip.
/v1/why/:runId/:taskIdOne task’s re-run verdict.
/v1/triage/:runIdBatched failure triage: every failed task’s “is this failure mine?” verdict — flaky (the same cache key succeeded in other runs), pre-existing (the default branch’s latest run of the task also fails), or new-failure (first failure of this key; keyChanged says whether this run altered the inputs) — with evidence run ids. Consumed by the run-detail triage card and by the CI plugin, which annotates failed rows in the GitHub job summary / check with these verdicts.
/v1/diff/:runId/:taskIdCache-key component diff vs the previous run.

taskId path segments are project#task, URL-encoded (app%23build).

All four validate a wire version where shown — a version-skewed client gets a 400 naming both versions. Bodies are read with a streaming cap: an over-cap (or chunked-without-length) body aborts with 413, never buffers. All are idempotent — a re-push of the same run/task deduplicates instead of duplicating rows.

RouteCapBody
POST /v1/ingest32 MiBA RunSummaryRecord — the end-of-run push; the completeness backstop for incremental task rows.
POST /v1/ingest/task2 MiBOne executed task’s result + retained log tail (wire v1), sent as the task finishes — what makes the run-detail page fill in live.
POST /v1/ingest/logs16 MiBThe end-of-run log-tail bundle (wire-versioned).
POST /v1/catalog8 MiBA workspace catalog push ({ v: 1, workspaceId, projects }) — resolved project/task metadata.

The workspace is routed from the pushing token’s org + the body’s client workspace id (auto-provisioned on first push); a workspace-scoped token is refused a foreign workspace (403).

HEAD/GET/PUT /v1/cache/:hash (hex hash) and POST /v1/cache/batch ({ hashes: […] }, ≤1024 per call, advertised as cacheWire: 2 on /v1/meta). Trust-scoped, immutable (re-PUT → 409), zstd-gated, S3-offloaded via 307 presigned GETs. Semantics + headers: Wire protocol; trust tiers + sub-scopes: Remote caching.

GET /v1/artifacts?limit= (default 200, max 1000) lists the store the caller’s read scopes can reach, with best-effort producing-task provenance — a session or token surface.

  • GET /events / GET /v1/events — SSE stream of your org’s distributed-run envelopes.
  • GET /stream — the same as NDJSON.
  • GET /v1/agents?ws=&session=&commit= — pool capacity probe (agents, remoteAgents, capacity, remoteCapacity, ready); the same path upgrades to the agent WebSocket. See Distributed CI.

Cross-origin browser connections are refused unless allow-listed (VX_CLOUD_ALLOW_ORIGIN); ?token= works on these three.

POST /mcp — JSON-RPC 2.0, seven read tools over the same analytics, org/workspace-clamped like every read. See MCP over HTTP.

  • 400 — missing/invalid required field, malformed request path, wire version mismatch (the body names both versions).
  • 401 — no credential on a gated surface.
  • 403 — session on a machine-token-only surface; ingest without a token; missing x-vx-csrf on a session mutation; insufficient role for an admin mutation.
  • 404 — unknown id, unknown ?ws=, or an org/workspace outside your tenancy (existence is never confirmed cross-tenant).
  • 409 — immutability (cache re-PUT), duplicate email/slug, already a member.
  • 413 — body over its streaming cap.
  • Malformed numeric query params degrade to the default rather than erroring.