Reference
Agent API
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.
| Route | What it does | Role |
|---|---|---|
| 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 /metrics | Counters 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/status | Name, 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/login | A 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/logout | Closes the session of the token carried. | open |
| GET /api/me | Who 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/users | The 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/tags | Every 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/apps | The 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>/versions | The 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/flows | The flows running on the agent: app, flow, nodes, messages, last errors. | view |
| GET /api/alarms | The 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/history | What 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/logs | The 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/stats | The historian: tags, records, bytes, the oldest sample, and the tags skipped for holding text. | view |
| GET /api/recipes | The recipes on this station, newest change first: { id, name, note, entries: [{ tag, value }], updated }. | view |
| POST /api/recipes | Writes 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>/apply | Writes 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/jobs | The jobs on this station, each with its cron expression, whether it is enabled, when it last ran and how that went. | view |
| POST /api/jobs | Writes 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>/run | Does 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/audit | The audit trail, newest last: ?since=<seq>, ?limit=<n> (500), ?event=<event>, ?name=<who>; ?format=csv downloads it as CSV. | engineer |
| GET /api/audit/verify | Walks 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/sources | The sources of this agent with their state and tag count, secrets masked. | view |
| POST /api/sources | Adds 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/templates | The 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/oidc | Whether 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 /ws | First, 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.