REST API
Dashboard HTTP API reference — the endpoints served on port 9002 that power the web console.
The Dashboard REST API runs on port 9002 (listen_port + 2, configurable via dashboard_port in flo.toml). It powers the web console and can be used directly for monitoring, management, and ad-hoc operations.
Read endpoints serve projection data directly. Write endpoints are proposed through the node's normal replication path, so they are durable just like a CLI or SDK call.
Base URL
http://localhost:9002/api/v1
Authentication
GET /health is always public. The /api/v1/* endpoints require auth only when a key store exists (i.e. after flo server bootstrap); a dev node started without bootstrap serves them openly.
When auth is enabled:
POST /api/v1/auth/session # body: { "api_key": "flo_sk_..." } → { "token": "..." }
GET /api/v1/auth/status # { "auth_enabled": true, ... }
Send the session token as a Bearer header on every request:
Authorization: Bearer <token>
Conventions
- Namespaces — Most resources are namespace-scoped. Pass
?namespace=<ns>(defaults todefault). - List vs. detail — A collection endpoint (e.g.
GET /streams) returns an array of summaries; the detail endpoint (GET /streams/:name) returns the full record.
Cluster & metrics
GET /api/v1/cluster/stats
Cluster health, throughput, and the node/shard list.
{
"rps": 45,
"active_connections": 12,
"uptime": "0d 4h 12m",
"version": "0.12.0",
"num_shards": 8,
"commands_total": 1234567,
"bytes_received": 9876543,
"bytes_sent": 5432198,
"subscriptions": 3,
"nodes": [
{ "id": "shard-0", "status": "healthy", "role": "active" }
]
}
GET /api/v1/metrics
Aggregated metrics in JSON (Prometheus metrics are exposed separately on port 9001).
{
"server": {
"connections": 12,
"subscriptions": 3,
"commands_total": 1234567,
"bytes_received": 9876543,
"bytes_sent": 5432198,
"uptime_seconds": 15120
},
"streams": 5,
"queues": 3,
"kv_namespaces": 4,
"workflows": {
"active_runs": 2, "started_total": 40, "completed_total": 31,
"failed_total": 4, "cancelled_total": 1, "timed_out_total": 0,
"signals_delivered_total": 6, "timers_fired_total": 0,
"steps_executed_total": 120, "active_schedules": 1
}
}
GET /health
Always-public liveness check (served at the root, not under /api/v1).
Namespaces
GET /api/v1/namespaces
[
{
"name": "production",
"stream_count": 2, "queue_count": 1, "kv_count": 7,
"workflow_count": 1, "processing_count": 3, "action_count": 3,
"is_system": false
}
]
POST /api/v1/namespaces
Create a namespace. Body: { "name": "production" }.
GET /api/v1/namespaces/:namespace
Namespace detail. Sub-collections: GET /namespaces/:namespace/streams, /queues, /kv.
KV
GET /api/v1/kv/namespaces
[
{ "name": "production", "key_count": 7, "bytes_stored": 1840,
"get_ops": 0, "set_ops": 12, "delete_ops": 0 }
]
GET /api/v1/kv/namespaces/:namespace/keys?prefix=<p>&limit=<n>
Scan keys. Returns { keys: [{ key, size, version }], count, has_more, cursor, namespace } (here version is the entry LSN).
GET /api/v1/kv/namespaces/:namespace/keys/:key
{
"found": true, "key": "user:42", "namespace": "production",
"value": "...", "version": 3, "size": 128,
"updated_at": 1718900000000, "ttl_ms": null
}
version here is the MVCC version count.
GET /api/v1/kv/namespaces/:namespace/keys/:key/history
{ "key": "user:42", "namespace": "production", "version_count": 3,
"versions": [{ "version": 3, "term": 1, "timestamp_ms": 1718900000000, "size": 128, "tombstone": false }] }
PUT /api/v1/kv/namespaces/:namespace/keys/:key
Write a value. Body: { "value": "...", "ttl_seconds": 3600, "nx": false }.
DELETE /api/v1/kv/namespaces/:namespace/keys/:key
Delete a key.
Streams
GET /api/v1/streams?namespace=<ns>
[
{ "name": "events", "namespace": "production", "partitions": 16,
"ingest_rate": 0, "reads": 0, "retention": "7d" }
]
GET /api/v1/streams/:name?namespace=<ns>
Detail with partitions[] (record counts + bytes) and consumer_groups[] (members, pending, last-delivered cursor).
GET /api/v1/streams/:name/messages?namespace=<ns>&limit=<n>
Records with id_ms, id_seq, ual_index, size, and the decoded payload. Sub-resources: GET /streams/:name/groups/:group, /groups/:group/pending, /groups/:group/members.
Queues
GET /api/v1/queues
[
{ "name": "tasks", "namespace": "production", "ready": 24, "inflight": 0,
"pending": 24, "available": 24, "enqueued": 30, "dequeued": 6, "dlq_count": 0 }
]
GET /api/v1/queues/:name?namespace=<ns>
Per-queue detail (same shape as the list item).
GET /api/v1/queues/:name/messages?namespace=<ns>&limit=<n>
Messages with seq, priority, state (ready / leased / dlq), attempts, lease_remaining_ms, size, and payload.
GET /api/v1/queues/:name/dlq?namespace=<ns>
Dead-letter entries.
Writes
POST /api/v1/queues/:name/purge?namespace=<ns>
POST /api/v1/queues/:name/dlq/:seq/requeue?namespace=<ns>
DELETE /api/v1/queues/:name/dlq/:seq?namespace=<ns>
Time Series
Measurements are namespace-scoped — pass ?namespace=.
GET /api/v1/timeseries?namespace=<ns>
[ { "name": "cpu_usage", "series_count": 1, "field_count": 1, "points": 60 } ]
GET /api/v1/timeseries/:measurement?namespace=<ns>
Detail: { name, namespace, field_count, fields: [{ name, type }], series_count, retention }.
GET /api/v1/timeseries/:measurement/data?field=<f>&namespace=<ns>&from=<ms>&to=<ms>
{ "measurement": "cpu_usage", "field": "value", "series": [ { "timestamp": 1718900000000, "value": 41.1 } ] }
GET | POST /api/v1/timeseries/floql
Execute a FloQL query (?q= or a raw body).
Actions
GET /api/v1/actions?namespace=<ns>
[
{ "name": "send-email", "namespace": "production", "type": "user",
"version": 1, "enabled": true, "timeout_ms": 30000, "max_retries": 3,
"worker_count": 1,
"runs": { "total": 14, "pending": 7, "running": 0, "completed": 6,
"failed": 1, "cancelled": 0, "timed_out": 0 } }
]
GET /api/v1/actions/:name?namespace=<ns>
Detail with runs, recent_runs[] (input/output/error/source), and the workers[] handling the action.
GET /api/v1/actions/:name/runs?namespace=<ns>&limit=<n>
Run history.
POST /api/v1/actions/:name/invoke?namespace=<ns>
Invoke (async). Body is the input JSON. Returns { ok, action, namespace, status, run_id }.
Workers
GET /api/v1/workers?namespace=<ns>
[
{ "worker_id": "worker-1", "status": "active", "worker_type": "action",
"namespace": "production", "machine_id": null, "current_load": 0,
"max_concurrent": 10, "tasks_completed": 6, "tasks_failed": 1,
"last_seen": 1718900000000, "registered_at": 1718900000000, "metadata": null,
"processes": [ { "name": "send-email", "kind": "action",
"run_count": 6, "fail_count": 1, "last_run_at": 1718900009000 } ] }
]
GET /api/v1/workers/:id
Single worker detail (same shape).
Processing
GET /api/v1/processing/jobs?namespace=<ns>
[
{ "job_id": "job-...", "name": "events-filter", "namespace": "production",
"status": "RUNNING", "parallelism": 1, "batch_size": 100,
"created_at": 1718900000000, "records_processed": 1000 }
]
GET /api/v1/processing/jobs/:id
Detail with the full pipeline yaml and savepoints[].
Writes
POST /api/v1/processing/jobs?namespace=<ns> # body: pipeline YAML → { ok, job_id, status }
PUT /api/v1/processing/jobs/:id/stop?namespace=<ns>
DELETE /api/v1/processing/jobs/:id?namespace=<ns> # cancel
Workflows
The console aliases workflows → workflow/definitions and workflows/:id → workflow/runs/:id.
GET /api/v1/workflow/definitions?namespace=<ns>
[
{ "name": "echo-workflow", "version": "1.0.0", "enabled": true,
"step_count": 1, "plan_count": 0, "has_schedule": false,
"has_trigger": false, "start_step": "start", "terminals": [], "steps": [] }
]
GET /api/v1/workflow/definitions/:name?namespace=<ns>
Detail with the full definition_yaml, status, and run_count.
PUT /api/v1/workflow/definitions/:name/enable?namespace=<ns>
PUT /api/v1/workflow/definitions/:name/disable?namespace=<ns>
GET /api/v1/workflow/runs?namespace=<ns>
[
{ "run_id": "wfr-...", "workflow": "echo-workflow", "version": "1.0.0",
"status": "completed", "triggered_by": "manual", "current_step": null,
"started_at": 1718900000000, "completed_at": 1718900000310,
"duration_ms": 310, "error": null, "history_event_count": 6 }
]
POST /api/v1/workflow/runs?namespace=<ns>&workflow=<name>&version=<v>
Start a run. The request body is the input JSON. Returns { ok, run_id, workflow, status }.
GET /api/v1/workflow/runs/:id
Run detail with current_step, timing, input, output, step_results, and pending_signals.
GET /api/v1/workflow/runs/:id/history
Event timeline: [{ event_type, step_name, timestamp }].
DELETE /api/v1/workflow/runs/:id?namespace=<ns> # cancel
POST /api/v1/workflow/runs/:id/signal?namespace=<ns> # body: signal payload
Error Format
Errors are returned as a JSON object with an error field:
{ "error": "Human-readable description" }
| HTTP Status | Meaning |
|---|---|
| 400 | Invalid request parameters |
| 401 | Missing or invalid auth token |
| 404 | Resource not found |
| 409 | CAS / version conflict |
| 429 | Server at capacity, retry later |
| 500 | Server error |
| 503 | Node not ready or shutting down |