Any signed-in account could save an ntfy channel with its own `server` and a
`{"$secret": ...}` token and press Test, which sent the stored secret to that
host. The config GET/PUT, the test route and the global search's alert entries
now answer to superusers, like /secrets. The webpush key and subscribe routes
stay open to every account, and the Alerts page shows a non-superuser only the
button for its own browser. The events table in the docs gains run_finished.
18 KiB
The HTTP API
The API covers the same operations as the browser. Use it for scripts, integrations, or your own client.
Set the base URL for your instance:
export FLUKSIO=http://127.0.0.1:8000/api/v1
Use the port printed by fluksio serve. For Docker, use
http://localhost:8080/api/v1, or https://<your-domain>/api/v1 for a
deployment.
All paths below are relative to this base. Timestamps in responses are
ISO 8601 and always carry their UTC offset.
Authenticating
fluksio serve signs the local CLI in automatically. To reuse its token
from a project instance, with jq installed:
export TOKEN=$(jq -r .token .fluksio/client.json)
Adjust the path for --global or --data-dir. For a remote instance,
use its login endpoint with the administrator credentials:
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 over MCP use separate OAuth tokens.
!!! tip "Interactive API reference"
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 | Action |
|---|---|---|
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 |
POST |
/flows/{name}/duplicate |
copy into an unpublished {name}_copy; code nodes need a superuser |
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 |
Deleting a flow keeps its run history. Delete runs separately.
Node source
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 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.
Ordinary source edits stay in the draft until publishing. Shared library
source changes affect its users immediately, and the audit trail records them
like a publish.
POST /flows/{name}/nodes/{id}/share promotes a source to the library;
/unshare makes it private again. GET /flows/library lists shared sources.
DELETE /flows/library/{name} retires one, and answers 409 while nodes still
use it. With ?detach=true each of those nodes keeps a copy of the code, in
the published flow and the draft, and the shared source is deleted.
Running and controlling
| Method | Path | Action |
|---|---|---|
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 |
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
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 "$FLUKSIO/messages/house.temperature/history" \
-H "Authorization: Bearer $TOKEN"
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 | Action |
|---|---|---|
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 |
GET |
/runs/{id}/nodes/{node}/outputs |
read what one node returned |
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. history=true also clears its cascades from the history |
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 |
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.
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.
A node's outputs come from the stage cache, so only a node that succeeded
and may be cached has them, up to 256 kB of JSON. Any other node answers
404.
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. An export covers at most the newest 10 000 runs. When a
selection matches more, the response carries X-Truncated with that cap and
X-Total-Count with the number of runs the selection matched.
Import takes {"runs": [...]}, records completed results without executing
code, and deduplicates by external_id. See
Runs for examples, artifacts, caching, and exports.
Artifacts
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
The upload returns a reference with the content digest. Identical bytes are
stored once. Node code uses save_artifact and load_artifact instead.
?volatile=true holds the bytes in the engine's memory for a live frame, where
newer frames push them out, instead of storing them. Such a frame is at most
MAX_FRAME_BYTES (4 MB), and a larger one is refused with 413. A camera only
superusers may see answers 403 to anyone else for the frames its messages
hold, the last 8 included (see Who may watch a camera).
A superuser can remove stored bytes with DELETE /artifacts/<digest>. While
a run, a current message, a flow input's initial value or a node setting in a
published or draft flow still references the digest, the call returns 409.
The hourly sweep removes unreferenced bytes on its own, so this is for
reclaiming space sooner.
Dashboards and devices
| Method | Path | Action |
|---|---|---|
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, /dashboards/{name}/discard, /dashboards/{name}/rename |
publish, discard, or rename |
POST |
/dashboards/{name}/duplicate |
copy into an unpublished {name}_copy |
GET |
/devices/ |
list devices, with online, last_seen, report and reported_at |
POST, GET, PUT, DELETE |
/devices/{id} |
create, read, save or delete one; a save sends the version it read and gets 409 when someone saved first. A read adds io and bindings, its ports as the device nodes of published flows wire them |
POST |
/devices/pair |
start device pairing, optionally with {name, board, firmware}; answers code, secret and the two-character check the device shows beside its code; unauthenticated, five a minute per address (the portal limits its own), the oldest of 50 waiting requests from the same side gives way |
GET |
/devices/pair/{code} |
poll pairing with its secret; answers device (and panel for older firmware); unauthenticated |
GET |
/devices/pending |
devices waiting for a code, as they describe themselves, each with its check |
GET |
/devices/pair/{code}/device |
what is waiting on a code, before it is approved |
POST |
/devices/{id}/pair |
adopt by {code}, or by {request} from the pending list for a request from your network |
POST |
/devices/{id}/unpair |
revoke the device credential, one the portal issued included |
PUT |
/devices/{id}/report |
what the device has, from its own credential only; at most 16 KB, and 64 KB once its named config_schemas are put in place (413 above either), replaces the last report, read leniently (see What a device reports) |
GET |
/devices/{id}/ice |
STUN and TURN servers for a live view of the device, as {ice_servers: [{urls, username?, credential?}], ttl}, with TURN credentials of the caller's own; 403 for whoever may not watch its camera, and for a device credential |
/panels/pair, /panels/pair/{code} and GET /panels/{id} answer as their
/devices/ counterparts, for firmware that has not updated yet.
Secrets, modules, and alerts
| Method | Path | Action |
|---|---|---|
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 |
GET |
/alerts/webpush/key |
the key a browser subscribes against; any account |
POST |
/alerts/webpush/subscriptions, /alerts/webpush/unsubscribe |
add or remove the calling browser; any account |
Secrets, package changes and the alert configuration require a superuser. Any account can subscribe its own browser. 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.
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 |
POST |
/observability/flush |
write buffered metrics and events now; superuser only |
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.
Live events
Connect to ws://127.0.0.1:8000/api/v1/flows/ws with an
Authorization: Bearer <access-token> header.
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.
A client asks for the bytes of the media it shows with
{"type": "media", "names": [...]}, which replaces what it asked for before,
for at most 8 messages, the last 8 it names when it names more. Each frame held in memory then arrives as a binary
message before the value naming it: four bytes of header length (big endian),
a JSON header {"type": "media", "name", "digest", "media_type", "ts"}, then
the bytes. Messages are sent without per-message compression.
A device's socket is bounded to its own messages. Its snapshot carries
"demand", the ports it reported as on_demand that someone wants now, and
"uplink", "binary" on its own socket and "http" through the portal. When
that set changes it is sent {"type": "demand", "ports": [...]}. A port has
demand while a socket asks for the media of the message it is wired to, or a
node of a running flow reads that message. A socket that falls so far behind
that events are dropped for it is sent a fresh snapshot, demand included, and
is closed with 1008 if it was unpaired meanwhile.
A person who may not watch a camera (see Who may watch a camera) gets none of its values in the snapshot or after it, and no frames when asking for them. Switching the camera's setting sends that person's open sockets a fresh snapshot.
With a binary uplink the device sends a camera frame as a binary message in
the same layout, with the header {"type": "frame", "name": "<message>", "media_type": "image/jpeg", "ts": <device ms>}. A microphone's chunk travels
the same way with a media type such as audio/L16;rate=16000. The message has
to be one of its in ports wired as image or audio, the media type of
that family, and the whole message at most MAX_FRAME_BYTES. The frame is
held in memory and published in one step, named after the message and credited
to its device node. A frame that does not
qualify is dropped, the reason logged once per socket, and the socket stays
open. A value of one of its in ports wired as bool, int, float or str
goes as the text message {"type": "publish", "name": "<message>", "value": ...}, taken in turn with the frames, so a microphone's listening false
arrives after its last chunk. With http it uploads with
PUT /artifacts?volatile=true&name=<message> and publishes with
POST /messages/<message>. A device that is unpaired or
deleted while connected is closed with 1008, at the latest with its next frame.
Live view
A device that reports a live port streams its camera over WebRTC. The
video goes peer to peer, or through a TURN relay, and the socket carries only
the signalling, as text, so it crosses the portal too. A person's socket
sends and hears {"type": "live", "device": "<id>", "session": "<id>", "action": ...}, a device's socket {"type": "signal", "session": "<id>", "action": ...}. A session id is 8 to 64 letters, digits, - or _, chosen by
the viewer. In order:
| Step | Viewer's socket | Device's socket |
|---|---|---|
| 1 | sends start |
hears start with ice_servers minted for the device |
| 2 | hears sdp (the offer) with ice_servers minted for the viewer |
sends sdp with its offer |
| 3 | sends sdp with its answer |
hears sdp |
| 4 | sends and hears candidate, the bare candidate:... line |
hears and sends candidate |
| 5 | sends bye, or hears bye or busy with a reason |
hears bye with a reason, or sends bye or busy |
A superuser may watch any device, anyone else a device whose camera is open
to members, and start is answered bye with forbidden otherwise. A device
speaks only for the sessions it was handed on that socket. The server answers
busy itself once a device has 2 sessions, and bye with offline or
unsupported when the device is not connected or reports no live port. A session the viewer has not
answered within 10 seconds ends with timeout, and either socket closing ends
its sessions with closed or offline. A socket that falls 64 signalling
messages behind ends the session with overflow instead of losing one.
ice_servers follow RTCIceServer: {urls: [...]} for STUN,
{urls, username, credential} for TURN, in coturn's REST scheme with a
username of <expiry>:<who>, good for an hour. A device that sends its camera's
frames on the same socket pauses them while a session negotiates, so its
offer and candidates are not queued behind frames.
Errors and client generation
| Code | Meaning |
|---|---|
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 |
For a version conflict, read current_version, reload, merge your edits, and
save again. Other 409 responses describe their own conflict.
make generate-client regenerates the frontend SDK. To generate a client in
another language, use the instance's /api/v1/openapi.json schema.