On this page · 2 sections

Every route answers JSON unless it serves a page or a file, and takes the token as Authorization: Bearer <token> - never in the URL: ?token= is refused. Without auth in the agent's configuration every role below reads as "open". A browser page calls the API from the agent's own origin, from the same machine, or from an origin listed in the configuration's origins; any other page is answered 403 and gets no CORS headers. An address is limited to limits.requestsPerMinute (600) calls, a body to limits.bodyBytes (8 MiB). See security for the whole fence.

RouteWhat it doesRole
GET /The agent's home page: apps, flows, alarms, sources with their link and state, tags.open
GET /apps/<name>/A published app. ?page=<id> opens a page, ?kiosk=1 hides the page tabs.open
GET /healthz{ ok, version, uptime, sources, tags }, 200 while the agent serves and 503 while it stops: for a watchdog or a load balancer.open
GET /metricsCounters and gauges in the Prometheus text format: tags, clients, sessions, samples, writes, refused requests, a gauge per source, a counter per flow.view
GET /api/statusName, version, uptime, auth on or off, users and sessions, TLS, the caller's role, native libraries, the licence, the fence (origins, allowCode, limits, the audit count), sources with state and link, apps.view
POST /api/loginA JSON body { "name", "password" }; answers { token, name, role, expires }. Five failures from one address block it for five minutes; twenty logins a minute from one address are the limit.open
POST /api/logoutCloses the session of the token carried.open
GET /api/meWho the caller is: name, role, kind (token, session, or open when auth is off).view
POST /api/me/password{ "current", "password" }: a signed-in user changes their own password (at least 10 characters). A token has none: 403. Recorded as user.password, a wrong current one as user.password.refused.view (a session)
GET /api/usersThe users of the station { name, role }, its tokens { name, role, hashed } and its identity provider, with me, the caller. No hash is in the answer. A station without sign-in answers 409: its first engineer is made on the station with --add-user.engineer
POST /api/users{ "name", "role", "password" }: adds a user (201, a password needed) or changes one (200; the password optional). Taken at once, written to auth.users in the configuration; a changed role or password ends the sessions of that user (sessionsEnded). The last engineer is not demoted (409). Recorded as user.add or user.change.engineer
DELETE /api/users/<name>Removes a user and ends their sessions. Not yourself and not the last engineer (409). Recorded as user.remove.engineer
GET /api/tagsEvery tag with its latest value, quality, unit and whether it is writable.view
POST /api/tags/write{ "tag", "value", "signature" }: writes a tag (a script, or the signing panel). With a signature (userId, userName, meaning, reason, at, recordHash, id) the session must be the signer's - the login was the second component - and the trail records the write with the signature; a signature by another user is refused and recorded.operate
GET /api/appsThe published apps.view
POST /api/apps/<name>Publishes the project in the body (up to limits.bodyBytes); starts its agent-side flows. Returns the app's URL, the SHA-256 of the project, how many code nodes it has, and the flows started. Refused with the nodes named when the project has Function nodes and the agent does not allow code.engineer
DELETE /api/apps/<name>Removes an app and stops its flows.engineer
GET /api/apps/<name>/versionsThe versions the agent kept of an app: { current, versions: [{ n, hash, date, by, title, pages, flows }] }; an empty list for an app not published yet.view
POST /api/apps/<name>/rollback{ "version": n }: that version is served again and its flows restarted; the trail records it.engineer
GET /api/flowsThe flows running on the agent: app, flow, nodes, messages, last errors.view
GET /api/alarmsThe alarm summary: the project alarms of every app the station serves, evaluated here whether or not a screen is open, and the ones its flows raised - { id, app, tag, message, priority, area, active, acknowledged, acknowledgedBy, timestamp, value, shelvedUntil }, worst first. See Alarms.view
GET /api/alarms/historyWhat happened to the alarms, from alarms.jsonl: ?app=&from=&to=&limit= (ISO times; the newest 500 by default, 5000 at most), each { at, app, id, tag, event, priority, message, value, user } with event raised, cleared, acknowledged, shelved or unshelved.view
POST /api/alarms/acknowledge{ "app", "ids": [...] } (["*"] for every outstanding one): answers { acknowledged }, the ids that changed. The trail records it as alarm.acknowledge with the user.operate
POST /api/alarms/shelve{ "app", "id", "minutes" } (1 to 1440, 60 when absent): hides the alarm from the summary for that time; it is still evaluated. POST /api/alarms/unshelve { "app", "id" } brings it back. Both are recorded in the trail.operate
GET /api/logsThe CSV and TDMS logs, the rotated ones (<name>-<date>.csv) among them; /api/logs/<app>/<name>.csv or .tdms downloads one.view
GET /api/history?tags=<a,b>&from=<ms>&to=<ms>&max=<n>Recorded samples in time order, as { tag, value, quality, timestamp } - the shape the Connect layer of the components asks for. Asked for more than max, the range is bucketed and each bucket gives up its lowest and highest, so the count is bounded and the spikes survive. A query naming no tag is refused.view
GET /api/history/statsThe historian: tags, records, bytes, the oldest sample, and the tags skipped for holding text.view
GET /api/recipesThe recipes on this station, newest change first: { id, name, note, entries: [{ tag, value }], updated }.view
POST /api/recipesWrites or replaces one: { "name", "entries": [{ "tag", "value" }] }, the id taken from the name when none is given. An entry with no tag or no value is refused here rather than when somebody loads it.engineer
POST /api/recipes/<id>/applyWrites every entry through the station and reports each: { applied, of, failed, entries, by, at }. Every entry is attempted; one that half-applies answers 207 and names the half that did not. The trail records it as recipe.apply.operate
DELETE /api/recipes/<id>Removes a recipe.engineer
GET /api/jobsThe jobs on this station, each with its cron expression, whether it is enabled, when it last ran and how that went.view
POST /api/jobsWrites or replaces one: { "name", "when", "action" }. when is { daily }, { weekly }, { every } or { cron }; action is { kind: "recipe" | "write" | "flow" }. A schedule or an action that cannot be read is refused here, not at two in the morning.engineer
POST /api/jobs/<id>/runDoes now what the clock would do, by the same path: { ok, at, why, detail }, or { ok: false, error }. The trail records it as job.run or job.failed.operate
DELETE /api/jobs/<id>Removes a job, and its run history with it.engineer
GET /api/auditThe audit trail, newest last: ?since=<seq>, ?limit=<n> (500), ?event=<event>, ?name=<who>; ?format=csv downloads it as CSV.engineer
GET /api/audit/verifyWalks the trail's hash chain: { ok, entries, first, last }, or { ok: false, brokenAt, reason } naming the first entry that no longer follows.engineer
GET /api/sources/<id>/browse?node=<nodeId>The children of an OPC UA node: name, nodeId, nodeClass. The Objects folder when node is empty.view
GET /api/sourcesThe sources of this agent with their state and tag count, secrets masked.view
POST /api/sourcesAdds a source and starts it: { "template": "keysight-34461a", "id": "dmm", "host": "10.0.0.31" } (a template plus an address) or a whole definition. It goes into the configuration file; the trail records it.engineer
DELETE /api/sources/<id>Stops and removes a source, from the configuration too.engineer
GET /api/templatesThe instrument templates the agent ships (templates/scpi/*.json): id, name, kind, maker, interfaces, tags, and the state of their path on the hardware page.view
GET /api/auth/oidcWhether the agent offers an identity provider: { enabled, issuer }, or why not.open
GET /api/auth/oidc/login?return=<path>Sends the browser to the provider (authorization code with PKCE); the callback /api/auth/oidc/callback opens a session and returns the browser to the path.open
GET /runtime/The Smart.Industrial package the apps load.open
GET /studio/The designer, served by the agent.open
WebSocket /wsFirst, when auth is on, { "type": "auth", "token" } (answered { "type": "auth", "role", "name" }); then { "type": "subscribe", "tag" }, unsubscribe, { "type": "write", "tag", "value" } (operate); samples arrive as { tag, value, quality, timestamp }, the latest on subscribe. A page of an origin not listed is refused at the handshake; a frame past limits.wsFrameBytes closes with 1009.view; operate to write

Samples over the WebSocket

{ "tag": "plc.TT-101.PV", "value": 72.3, "quality": "good", "timestamp": 1758361600000, "unit": "°C" }

Samples of one turn of the agent's event loop go out as one frame holding an array - a source that delivers a hundred tags at once sends a hundred samples in one message - and the runtime's adapter takes an array as it takes one sample; a client of your own must too. quality is good, bad (with error) or unknown. A write is confirmed by the next sample of the tag, read back from the device; the agent never echoes a write it has not read back. Errors come as { "type": "error", "tag", "error" }.

The virtual source flows

Values the agent-side flows publish (a Device write to flows.<name>) and the alarm summary ($alarms, an array of { id, tag, message, priority, area, active, acknowledged, acknowledgedBy, timestamp, value, shelvedUntil, app } - the project alarms of every app and the ones its flows raised, which carry ackBy instead of acknowledgedBy and no area; see Alarms) are tags like any other: listed by /api/tags under the source flows, subscribable over the WebSocket.