Runtime and modules
The runtime routes report the gateway's health and manage modules: what is installed, whether it runs, its log. Every module also gets a route namespace of its own under /api/v1/modules/{moduleId}, and every driver module answers the same baseline connection routes there. Background in Modules.
Health
| Method and path | Role | Purpose |
|---|---|---|
GET /api/v1/runtime/health | anonymous | {"status": "healthy"} or {"status": "unavailable"}; signed-in callers also get eventBackbone and modules |
GET /api/v1/health | anonymous | {"status": "ok", "time": "<ISO-8601>"} |
status says whether the internal event backbone is connected. For an authenticated caller the body adds eventBackbone (the backbone's health record) and modules, a summary per module: id, name, version, license, kind, technology, driver, state, error. Anonymous callers get only status: internal addresses, errors and what is installed are not for them.
Runtime replay over REST
GET /api/v1/runtime/replay?from=<ISO-8601>&limit=1000 (viewer) reads the backbone's retained metadata events and value batches from from onward, merged and ordered by timelineSequence, at most limit (1 to 10000) items:
[
{ "timelineSequence": 90311, "type": "value", "timestamp": "2026-11-08T12:00:00.123+00:00",
"value": { "tagId": 17, "sequence": 120, "value": 42, "quality": "good", "qualityDetail": null, "correlationId": null, "producerModuleId": "io.scadex.opcua" } },
{ "timelineSequence": 90312, "type": "metadata", "timestamp": "…", "metadata": { "…": "the tag metadata event" } }
]
This is the raw backbone window (EventBackbone__ValueRetention, default 7 days). Time-series reads for charts use tag.replay over the socket (see Live data over /sync) or the historian routes on History. The tag value routes under /api/v1/runtime/tags are on Tags.
Modules
| Method and path | Role | Purpose |
|---|---|---|
GET /api/v1/runtime/modules | viewer | Every installed module and its state |
GET /api/v1/runtime/modules/{id} | viewer | One module |
POST /api/v1/runtime/modules/{id}/start | engineer | Start a stopped or failed module |
POST /api/v1/runtime/modules/{id}/stop | engineer | Stop it, and keep it stopped across restarts |
POST /api/v1/runtime/modules/{id}/restart | engineer | Stop, reload the code, start |
POST /api/v1/runtime/modules?replace=false&start=true | admin | Install from a zip in the multipart field file |
DELETE /api/v1/runtime/modules/{id}?purgeData=false | admin | Remove an installed module |
PUT /api/v1/runtime/modules/{id}/log-level | engineer | Set the module's log level |
GET /api/v1/runtime/modules/{id}/logs | engineer | Read the log, newest first |
GET /api/v1/runtime/modules/{id}/logs/stream | engineer | Server-sent events |
GET /api/v1/runtime/modules/{id}/logs/download | engineer | The kept log files as text |
The module object
{
"id": "io.scadex.opcua", "name": "OPC UA", "version": "0.1.0", "vendor": "Scadex", "license": "…", "sdkVersion": "1.3",
"requiresLicense": false, "licensedTo": null, "licenseExpiresAt": null,
"kind": "driver", "technology": "opcua", "driver": { "…": "the driver experience descriptor" }, "description": "", "capabilities": [],
"state": "running", "error": null, "endpoints": [ { "…": "per endpoint" } ], "valueConsumer": null,
"origin": "…", "enabled": true, "lockedByConfiguration": false, "removable": true,
"startedAt": "…", "stateChangedAt": "…", "restarts": 0,
"log": { "level": "Information", "warnings": 0, "errors": 0, "lastError": null }
}
kind is driver, api, consumer or similar as the module declares it, or unknown until the module has loaded. state is one of starting, stopping, running, stopped, failed, disabled, incompatible, unlicensed, unsigned, tampered; error says why for failed and the refused states. lockedByConfiguration is true for a module in Modules__Disabled. Timestamps are ISO-8601. The lifecycle is described in Modules.
Start, stop, restart
Each answers with the module object: check state afterwards. Starting a failed module reads its folder afresh. Stopping sets enabled: false, so it stays stopped when the gateway restarts.
Install
POST /api/v1/runtime/modules as multipart/form-data with the zip in the field file. Query: replace (default false; set true to replace an installed version of the same id) and start (default true). A missing or empty file, or one over the size limit, is 400. Response:
{ "module": { "…": "the module object" }, "replacedVersion": "0.0.9" }
Remove
DELETE /api/v1/runtime/modules/{id}?purgeData=false. The module's connections, settings, data and tags stay, so installing it again picks up where it left off, unless purgeData=true, which deletes what the module stored itself. Tags are never deleted. Response:
{ "id": "io.scadex.historian", "purgedData": false, "tagsStillBound": 12, "restoredBuiltIn": false, "module": null }
Log level
PUT /api/v1/runtime/modules/{id}/log-level body {"level": "debug"}: trace, debug, information (or info), warning (or warn), error, critical. Anything else is 400. Returns the module object.
Reading the log
GET /api/v1/runtime/modules/{id}/logs?level=&search=&instance=&before=&after=&limit=200
| Parameter | Meaning |
|---|---|
level | Minimum level; default everything |
search | Text filter |
instance | Only lines tagged with this connection id |
before, after | Entry ids; pass nextBefore as before for the next page |
limit | 1 to 1000, default 200 |
Response { "entries": [...], "nextBefore": 8812 | null, "level": "Information" }.
GET /api/v1/runtime/modules/{id}/logs/stream?level=&after= answers text/event-stream: a : connected comment, the backlog since after, then each line as it is written as an event: log with the entry as JSON in data and the entry id in id, and a : keepalive comment every 15 seconds.
GET /api/v1/runtime/modules/{id}/logs/download streams the kept log files, oldest first, as text/plain named <id>.log. A gateway with no log files on disk sends the last 1000 buffered lines instead.
404 {"statusCode":404,"message":"There is no module '…'."} for an unknown id on any of these.
The module route namespace
A module's own endpoints are served under /api/v1/modules/{moduleId}/.... The gateway sets their policy, not the module: a GET needs viewer and any other method needs engineer, except endpoints the module marked as writing tag values, which operators may call. While a module is stopped, starting, stopping or failed its routes answer 503 with problem details titled Module unavailable; a module that was never loaded answers 404. See Errors.
The routes of the built-in modules: History for io.scadex.historian, Other routes for io.scadex.cron. OPC UA and MQTT are drivers and expose the baseline below plus whatever their descriptors add; see OPC UA and MQTT.
Baseline driver routes
Core maps these for every module that implements the driver contract, so every driver has the same connection API. {m} is the module id, for example io.scadex.opcua.
| Method and path | Role | Purpose |
|---|---|---|
GET /api/v1/modules/{m}/driver | viewer | The driver experience descriptor |
GET /api/v1/modules/{m}/connections | viewer | Every connection |
GET /api/v1/modules/{m}/connections/{connectionId} | viewer | One connection |
POST /api/v1/modules/{m}/connections | engineer | Create a connection |
PUT /api/v1/modules/{m}/connections/{connectionId} | engineer | Update a connection |
DELETE /api/v1/modules/{m}/connections/{connectionId} | engineer | Delete a connection (204) |
POST /api/v1/modules/{m}/connections/{connectionId}/test | engineer | Test it now |
GET /api/v1/modules/{m}/connections/{connectionId}/tags | viewer | The gateway tags bound to this connection |
POST /api/v1/modules/{m}/connections/{connectionId}/browse | engineer | Browse the device's address space |
POST /api/v1/modules/{m}/connections/{connectionId}/discover | engineer | Turn browsed ids into tag proposals |
Shapes
The descriptor (GET .../driver): { "connectionLabel", "connectionLabelPlural", "connectionFields": [...], "rootBrowseId" }. connectionFields lists the fields a connection's configuration takes; each driver's page documents them.
A connection:
{ "id": "…", "name": "S7_VN_NIRO", "enabled": true, "configuration": { "…": "driver-specific" }, "createdAt": "…", "updatedAt": "…" }
Create body: name (string, required), enabled (boolean, optional), configuration (object, required). Update body: all three optional. Unknown connection ids answer 404 {"message":"Connection not found."}.
Test result: { "success": true, "status": "connected", "message": null, "latencyMs": 12.4 }; status and message are the driver's words.
Browse body: { "parentId": null, "options": {} } (both optional; parentId absent means the root). Result:
{ "status": "ok", "message": null, "continuationToken": null,
"items": [ { "id": "ns=3;s=DB1.Motor1.Speed", "label": "Speed", "kind": "variable", "hasChildren": false, "dataType": 4, "binding": { "…": "ready to use as a tag's binding" }, "metadata": { } } ] }
Discover body: { "sourceIds": ["…"], "targetPath": "Plant.Line1", "options": {} } (sourceIds required). Result: a list of proposals { "sourceId", "suggestedPath", "kind", "dataType", "binding", "metadata" }, which the caller turns into tags with POST /api/v1/tags (see Tags).
Linked tags (GET .../tags): [{ "tagId", "path", "name", "enabled", "binding" }].
Every connection also has @gateway.connection.* status tags; see Connections.