HTTP API¶
Everything the web UI does goes through a JSON API under /api and three WebSocket endpoints
under /ws; scripts and other tools can use them the same way.
Conventions¶
- All endpoints are under
/api, speak JSON and return errors as{ "error": { "code", "message", "details"? } }. - Container ids accept the full id, the short id or the name.
- Long operations (launch, pull) return
202 { "jobId" }; follow the job via/api/jobs/:idor the/ws/eventssocket. A job is{ id, kind, meta, status, progress, result, error }withstatusrunning,doneorerrorandprogress: { message, current, total }. - When a token is configured every
/api/*request and/ws/*upgrade must carryAuthorization: Bearer <token>,X-Easy-MXL-Token: <token>or?token=<token>; otherwise401 unauthorized. State-changing requests from a foreign browser origin get403 origin_not_allowed; while EASY MXL is bound to loopback, a foreignHostheader gets403 host_not_allowed(Configuration). - Request bodies are JSON (
Content-Type: application/json, 1 MB limit). Boolean query flags such as?force=1accept1ortrue. - Common error codes:
400 validation_error,404 not_found/domain_not_found/flow_not_found/app_not_found/job_not_found,409for conflicts (see the table),502 docker_unavailablewhen the daemon cannot be reached.
REST endpoints¶
| Method and path | Body / query | Response |
|---|---|---|
GET /api/health |
{ ok, version, docker, domainRoot, domainRootFs, procLocksReadable, taiOffsetNs, taiSource } |
|
GET /api/apps |
catalog apps with imagePresent and their containers; local-image apps add imagePolicy, imageRepository, imageRepositories, localImages |
|
GET /api/apps/:id |
one app | |
POST /api/apps/:id/launch |
{ domain, name?, image?, hostPorts?, env?, params?, hostPaths?, launchRequires?, pull? } |
202 { jobId }; 409 name_conflict when the container name exists; 404 image_not_loaded for a local image that was not docker loaded; 400 image_repository_mismatch, unknown_dependency, invalid_pull_policy |
GET /api/containers |
container summaries (all states) | |
GET /api/containers/:id |
{ summary, inspect } |
|
POST /api/containers/:id/start (also stop, restart, kill) |
{ timeout? } |
{ ok, summary } |
DELETE /api/containers/:id |
?force=1&volumes=1 |
{ ok } |
GET /api/containers/:id/logs |
?tail=200×tamps=0 |
{ lines: [{ stream, text }] } (snapshot, max 5000 lines) |
GET /api/domains |
domains without flows | |
POST /api/domains |
{ name, label?, description?, id?, historyDurationMs? } |
201 domain; 409 domain_exists |
GET /api/domains/:name |
domain with flows |
|
POST /api/domains/:name/repair |
{ label?, description?, id?, force? } |
regenerates domain_def.json (fresh id, label defaults to the directory name); 409 domain_def_exists unless force |
PATCH /api/domains/:name |
{ historyDurationMs } (number, or null to reset to the 200 ms default) |
domain |
DELETE /api/domains/:name |
?force=1 |
{ ok }; 409 domain_in_use while running containers mount it, 409 domain_active while flows are active, unless forced |
GET /api/domains/:name/flows |
flows, each with writerContainer: { id, name } or null |
|
GET /api/domains/:name/flows/:flowId |
one flow | |
DELETE /api/domains/:name/flows/:flowId |
?force=1 |
{ ok }; 409 flow_active when a writer holds the lock, unless forced |
GET /api/domains/:name/containers |
containers that mount the domain | |
GET /api/jobs, GET /api/jobs/:id |
jobs (running / done / error with progress) |
|
GET /api/images |
[{ ref, present, id, size, created }] for catalog images |
|
POST /api/images/pull |
{ image } |
202 { jobId } |
GET /api/ports/check |
?port=9600&protocol=tcp |
{ port, protocol, container, listening } |
Launch parameters in detail:
| Field | Meaning |
|---|---|
domain |
domain name under the domain root; required when the app has a domainMount |
name |
container name override (default: the catalog containerName) |
image |
image reference override; for imagePolicy: local apps the loaded tag to run, its repository must be one of the app's imageRepositories |
hostPorts |
{ "9600/tcp": 9610 } - host port per container port spec |
env |
{ "KEY": "value" }, merged over the catalog env after templating |
params |
{ "groupHint": "cam-1" }, template values merged over the catalog defaults |
hostPaths |
{ "clips": "/srv/clips" }, absolute host path per hostPaths[].key |
launchRequires |
true (default) starts or launches the apps listed in requires first |
pull |
missing (default) or always; always is rejected for local images |
Domain names match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Flow objects carry id, label,
group, role, format, mediaType, summary, status (active / inactive / stale),
active (true, false or null when /proc/locks is unreadable), headIndex and
lastWriteTime in header, lastWriteAgeMs, latencyGrains, sizeBytes, writerPid and
writerContainer; the full shapes are in the
design contract.
Examples¶
export EASY_MXL_TOKEN=... # from /etc/default/easy-mxl
H='Authorization: Bearer '"$EASY_MXL_TOKEN"
B=http://localhost:9700
# health: Docker reachable? /proc/locks readable? TAI offset source?
curl -s -H "$H" $B/api/health
# create a domain with a 400 ms ring buffer
curl -s -H "$H" -H 'Content-Type: application/json' \
-d '{"name":"domain_1","label":"Studio 1","historyDurationMs":400}' $B/api/domains
# launch the Test Generator into it (returns a job id)
curl -s -H "$H" -H 'Content-Type: application/json' \
-d '{"domain":"domain_1"}' $B/api/apps/test-generator/launch
# -> {"jobId":"..."}
# follow the job until status is "done" or "error"
curl -s -H "$H" $B/api/jobs/<jobId>
# list the flows with their writer containers
curl -s -H "$H" $B/api/domains/domain_1/flows
# is host port 9600 free?
curl -s -H "$H" "$B/api/ports/check?port=9600&protocol=tcp"
# recreate a missing domain_def.json for a directory created by hand
curl -s -H "$H" -X POST -H 'Content-Type: application/json' -d '{}' $B/api/domains/domain_1/repair
# remove a stale flow left by a crashed writer (force skips the "not active" check)
curl -s -H "$H" -X DELETE "$B/api/domains/domain_1/flows/<flowId>?force=1"
# stop a container, then remove it together with its anonymous volumes
curl -s -H "$H" -X POST -H 'Content-Type: application/json' -d '{"timeout":5}' $B/api/containers/test-generator/stop
curl -s -H "$H" -X DELETE "$B/api/containers/test-generator?volumes=1"
WebSocket endpoints¶
Same authentication as the API (?token= is the practical choice for browsers). Unknown paths
are rejected before the upgrade completes; the server pings every socket every 30 s and drops
dead ones.
| Path | Server -> client | Client -> server |
|---|---|---|
/ws/events |
{type:'hello', version}, {type:'container', action, id, name, image, time}, {type:'image', action, id, time}, {type:'job', job} |
- |
/ws/containers/:id/logs?tail=×tamps= |
{type:'log', stream, text}, {type:'end'}, {type:'error', message} |
{type:'stop'} |
/ws/containers/:id/terminal?cols=&rows=&cmd= |
binary frames = terminal output; {type:'ready'}, {type:'exit', code}, {type:'error', message} |
binary frames = keyboard input; {type:'resize', cols, rows} |
/ws/events carries Docker container and image events plus every job update, which is how the
UI refreshes without polling. The terminal endpoint runs docker exec with a TTY; cmd, when
present, is split on whitespace and replaces the default shell (bash when available, else
sh). A terminal on a container that is not running fails with 409 not_running.
# watch events with websocat (any WebSocket client works)
websocat "ws://localhost:9700/ws/events?token=$EASY_MXL_TOKEN"