This commit is contained in:
2026-10-13 10:02:20 +02:00
parent a93f865e91
commit 80e595c02e
24 changed files with 2210 additions and 4310 deletions
+168 -201
View File
@@ -1,274 +1,241 @@
# The HTTP API
Everything the browser does, the API does first. The dashboard is a generated
client of this schema, not a privileged path into the engine, so anything you
can click, you can script.
The API covers the same operations as the browser. Use it for scripts,
integrations, or your own client.
Base URL: `https://api.${DOMAIN}/api/v1`, or `http://127.0.0.1:8000/api/v1` for
a `fluksio serve` instance.
Set the base URL for your instance:
```sh
export FLUKSIO=http://127.0.0.1:8000/api/v1
```
Use the port printed by `fluksio serve`. For Docker, use
`http://api.localhost/api/v1`, `http://<host-ip>:8080/api/v1` on a LAN stack,
or `https://api.<your-domain>/api/v1` for a deployment.
All paths below are relative to this base.
## Authenticating
```sh
export TOKEN=$(curl -s -X POST $FLUKSIO/login/access-token \
-d "username=admin@example.com&password=..." | jq -r .access_token)
`fluksio serve` signs the local CLI in automatically. To reuse its token
from a project instance, with `jq` installed:
curl -s $FLUKSIO/users/me -H "Authorization: Bearer $TOKEN"
```sh
export TOKEN=$(jq -r .token .fluksio/client.json)
```
A bearer token, valid for eight days. `POST /login/test-token` checks one.
Password recovery and reset are `POST /password-recovery/{email}` and
Adjust the path for `--global` or `--data-dir`. For a remote instance,
use its login endpoint with the administrator credentials:
```sh
export TOKEN=$(curl -s -X POST "$FLUKSIO/login/access-token" \
--data-urlencode "username=admin@example.com" \
--data-urlencode "password=<your-password>" | jq -r .access_token)
curl "$FLUKSIO/users/me" -H "Authorization: Bearer $TOKEN"
```
Session tokens last eight days by default. `POST /login/test-token` checks
a token. Password recovery uses `POST /password-recovery/{email}` and
`POST /reset-password/`.
Agents authenticate differently; see [Agents over MCP](agents.md).
[Agents over MCP](agents.md) use separate OAuth tokens.
!!! tip "The interactive schema"
!!! tip "Interactive API reference"
When `ENVIRONMENT` is not `production`, the full OpenAPI schema is at
`/docs` (Swagger) and `/redoc`, and the raw document at
`/api/v1/openapi.json`, for a browser on the engine's own machine (or any,
with `API_DOCS_ENABLED=true`). That is the authoritative reference; this
page is the tour.
It is closed in production: the schema enumerates every endpoint the
instance serves, including paths webhook nodes mounted at runtime.
Outside production, open `/docs` or `/redoc` on the API host.
The schema is at `/api/v1/openapi.json`. Access is limited to loopback
unless `API_DOCS_ENABLED=true`; the Docker development stack enables it.
Production disables these endpoints.
## Flows
| Method | Path | What |
| Method | Path | Action |
|---|---|---|
| `GET` | `/flows/` | every flow, with node counts, draft and running state |
| `GET` | `/flows/{name}` | one flow — the draft if there is one — with node status and issues |
| `PUT` | `/flows/{name}` | save a draft (`version` must match, or 409) |
| `POST` | `/flows/{name}/publish` | put the draft live |
| `POST` | `/flows/{name}/discard-draft` | throw the draft away |
| `POST` | `/flows/{name}/rename` | rename it |
| `DELETE` | `/flows/{name}` | delete it. Its runs stay — a run is the record of what ran, and only `DELETE /runs` clears one. Refused with 409 while a run of it is `running` or `queued` |
| `GET` | `/flows/node-types` | every node type and its parameter schema |
| `GET` | `/flows/graph` | every flow as one graph — what Home draws |
| `GET` | `/flows/` | list flows and their state |
| `GET` | `/flows/{name}` | read the draft when present, with status and issues |
| `PUT` | `/flows/{name}` | save a draft; send the current `version` |
| `POST` | `/flows/{name}/publish` | publish; send `{"version": <current-version>}` |
| `POST` | `/flows/{name}/discard-draft` | discard unpublished edits |
| `POST` | `/flows/{name}/rename` | rename |
| `DELETE` | `/flows/{name}` | delete; refused while a run is queued or running |
| `GET` | `/flows/node-types` | list types and parameter schemas |
| `GET` | `/flows/graph` | read the graph shown on Home |
Reading a flow returns the *draft* when one exists, along with a preview of how
it would run: node statuses and validation issues for the version you are
editing, not the one running underneath it.
Deleting a flow keeps its run history. Delete runs separately.
### Node source
```sh
curl -s $FLUKSIO/flows/house/nodes/decide/source -H "Authorization: Bearer $TOKEN"
curl -X PUT $FLUKSIO/flows/house/nodes/decide/source -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"code": "def process(x):\n return {\"y\": x * 2}\n"}'
curl "$FLUKSIO/flows/house/nodes/decide/source" \
-H "Authorization: Bearer $TOKEN"
curl -X PUT "$FLUKSIO/flows/house/nodes/decide/source" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"code": "def process(x):\n return {\"y\": x * 2}\n"}'
```
Writing a source is a superuser's, and so is any `PUT /flows/{name}` that
adds a Function or Command node, retypes or repoints one, changes a Command
node's settings, or changes any node's `resources.env`: each of them is code
the engine runs. Anyone else gets a 403 and keeps the rest — wiring, titles,
a Function node's settings and every other node.
Writing code requires a superuser. So does adding, retyping, or repointing
Function and Command nodes, changing Command settings, or changing
`resources.env`. Other users can edit wiring, titles, and ordinary settings.
`POST .../share` promotes a node's source to the shared library;
`POST .../unshare` gives it a private copy back. `GET /flows/library` lists the
shared sources and which flows use each.
Ordinary source edits stay in the draft until publishing. Shared library
source changes affect its users immediately.
`POST /flows/{name}/nodes/{id}/share` promotes a source to the library;
`/unshare` makes it private again. `GET /flows/library` lists shared sources.
### Running and controlling
| Method | Path | What |
| Method | Path | Action |
|---|---|---|
| `POST` | `/flows/{name}/start` · `/stop` | activate or tear down its subscriptions and schedules |
| `POST` | `/flows/{name}/pause` · `/resume` | hold messages instead of running them |
| `POST` | `/flows/{name}/step` | release exactly one held message |
| `POST` | `/flows/{name}/validate` | the issues, without saving |
| `POST` | `/flows/{name}/run` | run every node once, from the values you pass |
| `POST` | `/flows/{name}/nodes/{id}/trigger` | feed values into one node |
| `POST` | `/flows/{name}/nodes/{id}/cancel` | kill the worker running it right now |
| `POST` | `/flows/{name}/nodes/{id}/acknowledge` | clear the failure the canvas is marking |
| `POST` | `/flows/{name}/start`, `/flows/{name}/stop` | activate or stop a live flow |
| `POST` | `/flows/{name}/pause`, `/flows/{name}/resume` | hold or release incoming work |
| `POST` | `/flows/{name}/step` | release one held message |
| `POST` | `/flows/{name}/validate` | validate the current draft |
| `POST` | `/flows/{name}/run` | run the current draft once with `{"inputs": {...}}` |
| `POST` | `/flows/{name}/nodes/{id}/trigger` | feed a node with `{"values": {...}}` |
| `POST` | `/flows/{name}/nodes/{id}/cancel` | stop active node code |
| `POST` | `/flows/{name}/nodes/{id}/acknowledge` | clear its failure marker |
`POST /flows/{name}/run` on a **batch** flow submits a run instead, because
that is what running one means: the parameters, the series and the result are
the point.
A preview run can have real side effects. On a batch flow, `/run` submits a
recorded run; use the run endpoints below for parameters and tracking.
## Messages
```sh
curl -s $FLUKSIO/messages/ -H "Authorization: Bearer $TOKEN"
curl -X POST $FLUKSIO/messages/house.setpoint -H "Authorization: Bearer $TOKEN" \
curl "$FLUKSIO/messages/" -H "Authorization: Bearer $TOKEN"
curl -X POST "$FLUKSIO/messages/house.setpoint" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"value": 22.5}'
curl -s $FLUKSIO/messages/house.temperature/history -H "Authorization: Bearer $TOKEN"
curl "$FLUKSIO/messages/house.temperature/history" \
-H "Authorization: Bearer $TOKEN"
```
`GET /messages/` is the catalogue: every message any published flow declares,
with its last value. Publishing puts a value into the graph exactly as a
dashboard control does, which means only a message some flow *declares* can be
published to. Flows own the namespace; everything else is a client of it.
The catalogue contains messages declared by published flows and their last
values. You can publish only to declared messages, just as dashboard controls do.
## Runs
| Method | Path | What |
| Method | Path | Action |
|---|---|---|
| `POST` | `/runs/flows/{name}` | queue one run — `{"params": {...}, "seed": 7, "draft": false, "no_cache": false}`. `"cause"` says where it came from — `api` (the default), `cli` or `sdk` |
| `POST` | `/runs/flows/{name}/sweep` | queue many, sharing a `group_id` |
| `POST` | `/runs/import` | record runs executed somewhere else, up to 200 per request. Answers `[{"external_id", "id", "created"}]` |
| `GET` | `/runs` | the queryable history: `?flow=`, `?status=`, `?group=`, `?digest=`, `?since=`, `?before=`, `?limit=`, `?offset=` |
| `DELETE` | `/runs?flow=` | every run of one flow, and everything hanging off them. `flow` is required. Refused with 409 while one of them is still going |
| `GET` | `/runs/overview` | one row per flow that has runs, with how many are running or queued |
| `GET` | `/runs/export/metrics?…&name=&stride=&format=` | every selected run's series as one long table: `run, name, step, ts, value` |
| `GET` | `/runs/export/runs?…&params=&metrics=&format=` | one row per run: its inputs as columns, its final numbers, its status and provenance |
| `GET` | `/runs/metrics/names?…` | every metric name the selected runs recorded, distinct; takes the export's own filters |
| `GET` | `/runs/{id}` | one run in full: params, result, per-node record, artifacts |
| `POST` | `/runs/{id}/cancel` | stop it |
| `DELETE` | `/runs/{id}` | forget it, with its nodes, series and artifact rows. Cancel a live one first |
| `POST` | `/runs/{id}/retry` | run the same thing again, as a new run naming this one |
| `GET` | `/runs/{id}/metrics?name=&stride=` | one metric's series, in step order; every series of the run without `name` |
| `GET` | `/runs/series/compare?ids=a,b,c&metric=&x=` | that metric across several runs. `x` is what to plot against: nothing or `step`, `time` (seconds since each run's own first reading), or another metric's name, joined on the step the two share |
| `POST` | `/runs/flows/{name}` | queue a run with `{"params": {...}, "seed": 7}` |
| `POST` | `/runs/flows/{name}/sweep` | queue runs with a shared group |
| `POST` | `/runs/import` | import up to 200 completed runs |
| `GET` | `/runs` | query history |
| `GET` | `/runs/overview` | summarize runs per flow |
| `GET` | `/runs/{id}` | read parameters, results, nodes, and artifacts |
| `POST` | `/runs/{id}/cancel`, `/runs/{id}/retry` | cancel, or create a retry |
| `DELETE` | `/runs/{id}` | delete a finished run and its records |
| `DELETE` | `/runs?flow={name}` | delete a flow's runs; refused while any are active |
| `GET` | `/runs/{id}/metrics?name=&stride=` | read metric series |
| `GET` | `/runs/metrics/names` | list metric names across a selection |
| `GET` | `/runs/series/compare?ids=a,b,c&metric=&x=` | compare curves by step, time, or another metric |
| `GET` | `/runs/export/metrics`, `/runs/export/runs` | stream CSV or JSONL |
Submitting answers immediately with a `queued` run. Wrong parameters (an
undeclared name, a value of the wrong type) come back as a 422 naming the
problem, before anything executes.
Submissions return a queued run immediately. Invalid or undeclared parameters
return `422` before execution. Optional `draft` and `no_cache` flags select
unpublished code or skip stage caching.
`/runs/import` takes `{"runs": [...]}`. Each entry carries `flow`,
`external_id`, `params`, `seed`, `status` (`ok`, `error` or `cancelled`),
`created_at`, `started_at`, `finished_at`, `result`, `metrics` (`name`,
`step`, `ts`, `value`), `artifacts` (`name`, `filename`, `media_type`,
`content_base64`), `labels` and `group_id`. The parameters are completed and
checked as a submit's are, the run is stored with the cause `import` and an
empty commit and code digest, and nothing is queued. The request is checked
whole before anything is written, and an `external_id` already imported
answers with that run and `"created": false`.
History filters include `flow`, `status`, `group`, `digest`, `since`,
`before`, `limit`, and `offset`. Rows arrive newest first; use the last
row's `created_at` as `before` for stable pagination.
`?digest=` filters by the hash of the parameters, which is how you find "every
run that used exactly this configuration".
Exports also accept `ids` and `until`, with `format=csv` or `jsonl`.
The metrics export is a long table; the runs export is one row per run.
Use `params` and `metrics` to choose columns, including dotted paths into
record outputs.
`?before=` is how a long history is paged: rows come newest first, so handing
back the last row's `created_at` reads the next page whatever landed
meanwhile, which `?offset=` cannot, since a run submitted between two pages
shifts every row down one. `?since=` is inclusive and bounds the other end.
Both exports stream `csv` (the default) or `jsonl`, and take the selection the
history takes plus `?ids=a,b,c`, `?since=` and `?until=`. `export/runs` puts
the inputs that *vary* across the selection in `param.` columns (the sweep
axis) unless `?params=` names them, and the run's numbers in `metric.`
columns. Both are dotted paths into whatever a node returned, so
`?metrics=final_metrics.train_loss` selects a field of a record and the
defaults reach every number inside one. A key with a dot in its own name, such
as `-1.0`, cannot be addressed that way and gets no column. The run id is on every row of both, which is what makes an exported
file a join back to the run rather than a loose table.
`compare` answers in the same shape a chart widget draws, so three training
curves side by side is a widget binding rather than a screen of its own.
See [Runs: pipelines that finish](../concepts/runs.md).
Import takes `{"runs": [...]}`, records completed results without executing
code, and deduplicates by `external_id`. See
[Runs](../concepts/runs.md) for examples, artifacts, caching, and exports.
## Artifacts
```sh
curl -X PUT $FLUKSIO/artifacts -H "Authorization: Bearer $TOKEN" \
--data-binary @model.pt
curl -s $FLUKSIO/artifacts/sha256:abc... -H "Authorization: Bearer $TOKEN" -o model.pt
curl -X PUT "$FLUKSIO/artifacts?name=model.pt" \
-H "Authorization: Bearer $TOKEN" --data-binary @model.pt
curl "$FLUKSIO/artifacts/<digest>" \
-H "Authorization: Bearer $TOKEN" -o model.pt
```
Content-addressed, so uploading the same bytes twice stores them once. Node
code normally reaches these through `fluksio.save_artifact` /
`fluksio.load_artifact` rather than here.
The upload returns a reference with the content digest. Identical bytes are
stored once. Node code uses `save_artifact` and `load_artifact` instead.
## Dashboards and panels
| Method | Path | What |
| Method | Path | Action |
|---|---|---|
| `GET` | `/dashboards/` | every dashboard, without its contents |
| `GET` | `/dashboards/{name}?draft=true` | the published document, or the editor's copy |
| `POST` | `/dashboards/{name}` | create one |
| `POST` | `/dashboards/from-flow/{flow}` | build `{flow}_results` from a batch flow's declared ports — a chart per streamed metric, a stat per output. 409 if it already exists |
| `GET` | `/dashboards/` | list dashboards |
| `GET` | `/dashboards/{name}?draft=true` | read a draft; omit the query for published content |
| `POST` | `/dashboards/{name}` | create |
| `POST` | `/dashboards/from-flow/{flow}` | create a batch flow's results dashboard |
| `PUT` | `/dashboards/{name}` | save a draft |
| `POST` | `/dashboards/{name}/publish` · `/discard` · `/rename` | |
| `GET` `PUT` | `/panels/` | which device shows which dashboards |
| `POST` | `/panels/pair` | start a pairing |
| `GET` | `/panels/pair/{code}` | what is holding a code |
| `POST` | `/panels/{id}/unpair` | drop this panel's credential, keep the panel |
| `POST` | `/dashboards/{name}/publish`, `/dashboards/{name}/discard`, `/dashboards/{name}/rename` | publish, discard, or rename |
| `GET`, `PUT` | `/panels/` | read or update panel assignments |
| `POST` | `/panels/pair` | start device pairing |
| `GET` | `/panels/pair/{code}` | poll pairing with its `secret` |
| `POST` | `/panels/{id}/pair` | approve the pairing |
| `POST` | `/panels/{id}/unpair` | revoke the panel credential |
## Secrets, modules, alerts
## Secrets, modules, and alerts
```sh
curl -s $FLUKSIO/secrets/ -H "Authorization: Bearer $TOKEN" # names only
curl -X PUT $FLUKSIO/secrets/influx-token -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"value": "..."}'
curl -s $FLUKSIO/modules/ -H "Authorization: Bearer $TOKEN"
curl -X POST $FLUKSIO/modules/apply -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"requirements": "numpy>=2\n"}'
curl -s $FLUKSIO/alerts/config -H "Authorization: Bearer $TOKEN"
curl -X POST $FLUKSIO/alerts/test/my-phone -H "Authorization: Bearer $TOKEN"
```
Secrets are write-only over the API: you can list names and set values, never
read one back. Secrets and `POST /modules/apply` are a superuser's, and so is
what a connector keeps across restarts: `GET` or `DELETE
/connectors/{node_type}/state`, or `DELETE /connectors/{node_type}/state/{key}`
for one key (see [the connector contract](../reference/connector-contract.md#keeping-state-across-restarts)).
## Observability
| Path | What |
|---|---|
| `/observability/summary` | engine health — always 200, degraded or not — and the `version` it is running |
| `/observability/timeseries` | executions and failures over a window |
| `/observability/flows` | per-flow rollups with a 60-slice trend |
| `/observability/runs` | recent cascades, with `?flow=`, `?since=`, `?until=` |
| `/observability/events?kind=failure\|audit` | what went wrong, or who changed what; narrows by `?flow=`, `?run=`, `?since=`, `?until=` |
| `/observability/dead-letter` | work the engine gave up on |
`GET /utils/health/` is the deep health check the container probe uses: it
fails when the event loop is wedged or the state backend is gone, not just when
the process is dead.
## Workers
| Method | Path | What |
| Method | Path | Action |
|---|---|---|
| `GET` | `/workers` | what is attached, its labels and how busy it is |
| `POST` | `/workers/tokens` | mint a worker credential with its name and labels (superuser; shown once) |
| `GET` | `/workers/tokens` | the worker credentials minted, without the tokens (superuser) |
| `POST` | `/workers/tokens/{jti}/revoke` | refuse one from now on and drop a worker attached with it (superuser) |
| `GET` | `/workers/runtime` | the node runner's source, for a host without pip |
| `WS` | `/workers/attach` | where a worker dials in, with `Authorization: Bearer <worker token>` |
| `GET` | `/secrets/` | list names only |
| `PUT` | `/secrets/{name}` | set `{"value": "..."}` |
| `GET` | `/modules/` | read installed packages and the manifest |
| `POST` | `/modules/apply` | replace the manifest with `{"requirements": "numpy>=2\n"}` |
| `GET`, `PUT` | `/alerts/config` | read or update alert configuration |
| `POST` | `/alerts/test/{channel}` | test a channel |
See [Remote workers](workers.md).
Secrets and package changes require a superuser. Applying a manifest replaces
the whole managed environment's package list. Adopted environments are read-only
here; install their packages yourself.
Connector state keys can also be listed and cleared by a superuser; see
[the connector contract](../reference/connector-contract.md#keeping-state-across-restarts).
## Observability and workers
| Method | Path | Action |
|---|---|---|
| `GET` | `/observability/summary` | health and engine version; always returns `200` |
| `GET` | `/observability/timeseries`, `/observability/flows` | execution metrics or flow rollups |
| `GET` | `/observability/runs`, `/observability/events`, `/observability/dead-letter` | cascades, events, or abandoned work |
| `GET` | `/utils/health/` | deep health check; fails on loop or state-backend problems |
| `GET` | `/workers`, `/workers/resources` | attached workers and resource availability |
| `GET`, `POST` | `/workers/tokens` | list or mint worker credentials; superuser only |
| `POST` | `/workers/tokens/{jti}/revoke` | revoke a worker |
| `GET` | `/workers/runtime` | read node-runner source |
| `WS` | `/workers/attach` | attach with a worker token |
| `GET`, `POST` | `/flavors/` | list or create resource sizes |
| `PATCH`, `DELETE` | `/flavors/{name}` | update or delete a size |
Changing flavors requires a superuser. See [Remote workers](workers.md).
## Live events
```text
ws://127.0.0.1:8000/api/v1/flows/ws
```
Connect to `ws://127.0.0.1:8000/api/v1/flows/ws` with an
`Authorization: Bearer <access-token>` header.
A client that can set headers sends `Authorization: Bearer <access token>` on
the handshake. A browser cannot, so it first calls `POST /flows/ws-ticket` and
opens `/flows/ws?ticket=<ticket>`: a ticket is good for one connect, within 30
seconds, so the URL that access logs keep is worth nothing afterwards.
`?token=` is still accepted for one more release. The socket sends a snapshot
on connect and then every engine event: node executions, values published,
health changes, run started and finished. This is what the canvas and the
dashboards draw from.
Browsers first call `POST /flows/ws-ticket`, then connect with
`?ticket=<ticket>`. Tickets are single-use and expire after 30 seconds.
The socket sends a snapshot followed by node, value, health, and run events.
## Errors
## Errors and client generation
| Code | Means |
| Code | Meaning |
|---|---|
| `400` | the request was malformed, or the node refused it with a message |
| `401` / `403` | not signed in, or not allowed |
| `404` | no such flow, dashboard, run or message |
| `409` | someone else saved first — the body carries `current_version` |
| `422` | a parameter, port or binding did not typecheck |
| `503` | that subsystem is not available on this instance |
| `400` | malformed request or refused operation |
| `401` / `403` | missing authentication or permission |
| `404` | resource not found |
| `409` | version conflict, duplicate, or resource still in use |
| `413` | artifact upload exceeds the configured limit |
| `422` | invalid parameters, ports, or bindings |
| `503` | subsystem unavailable |
A 409 on a save or a publish is not an error to retry blindly: it means the
stored version moved past the one you were editing. Re-read, merge, save again.
For a version conflict, read `current_version`, reload, merge your edits, and
save again. Other `409` responses describe their own conflict.
## Generating a client
The frontend's TypeScript client is generated from the OpenAPI schema
(`make generate-client`). Any OpenAPI generator will do the same for your
language: point it at `/api/v1/openapi.json` on a non-production instance,
from the machine it runs on.
`make generate-client` regenerates the frontend SDK. To generate a client in
another language, use the instance's `/api/v1/openapi.json` schema.