REST API
Valved is headless by default. valved serve exposes a FastAPI REST API covering the core loop (plan, build, deploy, run) and the control plane (jobs, workers, pipelines, schedules, tokens, webhooks, metrics, memory), so an external agent, chat tool, or script can drive Valved without the CLI.
It is not a mirror of the CLI. Project setup — init, connect, auth, creating or changing targets, graduating a component, registering an MCP server, el run — has no REST equivalent, and the routers for targets, components, agents, skills and MCP servers are read-only. POST /plans accepts only {goal, pipeline_name}, so plan --refine and the destination pre-seeds have no endpoint either.
valved serve [--workers N] [--interval S]# binds from [api] host/port in valved/api.toml — default http://127.0.0.1:8765Discovering the surface
Section titled “Discovering the surface”The OpenAPI schema is served live, and there’s interactive Swagger:
curl -s http://127.0.0.1:8765/api/openapi.json # machine-readable schemaopen http://127.0.0.1:8765/api/docs # Swagger UIThe MCP server is generated from exactly this schema, so the REST surface and the MCP tool catalog are always in lockstep.
Health
Section titled “Health”curl -s http://127.0.0.1:8765/healthz # livenesscurl -s http://127.0.0.1:8765/readyz # readiness (200 once migrations are at head)Resource routers
Section titled “Resource routers”The API is organized into routers under /api/v1, mirroring the CLI groups:
| Router | Resource |
|---|---|
plans |
draft plans (create, refine, show) |
builds |
build runs against a plan |
pipelines |
pipeline definitions + lineage |
runs |
on-demand runs, logs, and the event stream |
schedules |
live schedule (pause/resume/set-cron), audited changes |
deploys |
promote built code via the configurable handoff |
jobs / workers |
the queue and worker pool |
asks / investigations |
read-only Q&A and recovery records |
components / targets |
component references and connection targets |
agents / skills / mcp_servers |
extensibility |
memory / metrics |
project memory and cost/usage rollups |
tokens / webhooks |
API token lifecycle and run-completion webhooks |
Requests carry a bearer token. valved serve bootstraps one to .valved/token on first run; mint or rotate with:
valved auth rotate # prints the new token once# then:curl -s http://127.0.0.1:8765/api/v1/pipelines \ -H "Authorization: Bearer $VALVED_API_TOKEN"Programmatic token lifecycle is REST-native: POST /api/v1/tokens and DELETE /api/v1/tokens/{id}.
Three endpoints back the explorer’s read-only Q&A: GET /api/v1/asks (cursor-paginated history, filterable by ?pipeline= and ?since=), GET /api/v1/asks/{ask_id} (one ask, with its full citations hydrated from the durable trace), and POST /api/v1/asks, which runs the explorer synchronously and returns the completed ask — 5–30 seconds is typical.
POST /api/v1/asks takes three body fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
question |
string | yes | The question to answer. |
pipeline |
string | no | Scope the investigation to one pipeline. |
target |
string | no | The target whose warehouse connection backs the explorer’s sql tool. |
curl -s "$BASE/asks" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"question":"what breaks if I change raw_stripe.charges?"}'target is not just a label on the ask row — it is resolved into the connection the explorer actually queries, so an ask over REST reaches your warehouse exactly as valved ask does (see the core loop). Because it now reaches target resolution, it is validated against the target-name regex ^[a-z][a-z0-9_]*$: a free-form label that earlier releases would have stored returns 400 with type https://valved.dev/errors/target-resolution. Values that follow the normal target-name rules are unaffected. Omit the field and Valved resolves VALVED_TARGET, then default_target — an unscoped ask still queries the project’s default target.
A failed ask is not an HTTP error. If the explorer cannot answer, POST /api/v1/asks still returns 200 with "status": "failed" and an error_message — check the body’s status, not the HTTP code.
Errors
Section titled “Errors”Every error is RFC 9457 application/problem+json: {type, title, status, detail, instance} plus endpoint-specific fields. type is https://valved.dev/errors/<slug>; switch on the slug, never on title or detail. The slugs you’ll meet on the asks endpoints:
| Status | Slug | When |
|---|---|---|
| 400 | target-resolution |
target fails the target-name regex (POST /asks). |
| 400 | bad-request |
A malformed ?since= window on GET /asks. |
| 401 | missing-bearer-token / invalid-token |
No Authorization: Bearer header, or an unrecognized token. |
| 404 | not-found |
GET /asks/{ask_id} for an unknown or non-UUID id. |
| 422 | validation |
Body failed schema validation — e.g. question omitted. |
Other routers surface their own slugs the same way — POST /builds returns 409 config-drift (with plan_id, expected_config_hash, actual_config_hash and a recovery_hint in the body) or 409 plan-expired; POST /plans returns 422 plan-generation. An unrecognized internal failure becomes a 500 internal with an opaque detail; the stack trace is logged server-side, never returned.
Streaming & webhooks
Section titled “Streaming & webhooks”- Run event stream —
GET /api/v1/runs/{run_id}/streamstreams live run events (this is whatvalved run --watchconsumes). Because it’s a stream, it is not exposed as an MCP tool. - Webhooks — register a webhook to receive run-completion callbacks; manage them under
/api/v1/webhooks(create, patch, rotate-secret, delete).
Example: plan → build → run over HTTP
Section titled “Example: plan → build → run over HTTP”BASE=http://127.0.0.1:8765/api/v1AUTH="Authorization: Bearer $VALVED_API_TOKEN"
PLAN=$(curl -s "$BASE/plans" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"goal":"ingest the Stripe charges API into raw_stripe"}' | jq -r .id)
curl -s "$BASE/builds" -H "$AUTH" -H 'Content-Type: application/json' \ -d "{\"plan_id\":\"$PLAN\"}"
curl -s "$BASE/runs" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"pipeline":"stripe_charges"}'