Skip to main content
Version: Next

Screens

Screens are the HMI pages the designer builds; the API calls them views, under /api/v1/views. Start screens are named entry points into them, under /api/v1/start-screens. Background in Screens and Start screens.

Routes​

Method and pathRolePurpose
GET /api/v1/views?withBody=trueviewerEvery screen, newest change first
POST /api/v1/viewsengineerCreate an empty screen
GET /api/v1/views/{id}viewerOne screen with its components
PATCH /api/v1/views/{id}engineerChange name, description, meta or folder
PUT /api/v1/views/{id}engineerSave name, meta and components in one step, with conflict detection
PUT /api/v1/views/{id}/componentsengineerReplace the components only
DELETE /api/v1/views/{id}engineerDelete a screen
GET /api/v1/views/foldersviewerFolder paths
POST /api/v1/views/foldersengineerCreate a folder
POST /api/v1/views/{id}/presenceengineerReport that the caller has the screen open in the editor
DELETE /api/v1/views/{id}/presenceengineerThe caller closed the editor
GET /api/v1/start-screens?all=viewerStart screens the caller may see, and the caller's choice
POST /api/v1/start-screensengineerAdd a start screen
PATCH /api/v1/start-screens/{id}engineerChange one
DELETE /api/v1/start-screens/{id}engineerRemove one
PUT /api/v1/start-screens/orderengineerReorder
PUT /api/v1/start-screens/meviewer (session only)The caller's own default and launch choice

Screens are stored as files in the project directory, not in the database. Their ids are generated by the gateway; there are no slugs. A screen is opened by id.

Screen objects​

Without components (list, create, PATCH):

{ "id": "…", "name": "Overview", "description": "", "folder": "Line1", "meta": "{}", "updatedAt": "2026-11-08T12:00:00.0000000+00:00", "_count": { "components": 12 } }

With components (GET /api/v1/views/{id}, GET /api/v1/views?withBody=true, PUT): the same fields plus components, an array of:

FieldTypeMeaning
typestringOne of the known component types (label, numeric, gauge, bar, button, toggle, indicator, led, sparkline, input, image, rectangle, header, template, tank, slider, dropdown, clock, multistate, trend, table, line, arrow, ellipse, polyline, polyshape, text, coord, flex, the sym-* process symbols, pipe, pipe-junction, instrument, status-indicator, command, setpoint, field, analog-bar, live-value, alarm-summary). Unknown types are 400
x, y, w, h, znumberPosition and size, rounded to integers. w and h are at least 8; defaults 160 × 80. z defaults to the array index
lockedboolean
configobject or JSON stringThe widget's settings; see Widgets
localIdstringId inside the screen; must be unique
parentLocalIdstringThe container (coord or flex) this component sits in
namestring
layoutobject or JSON stringLayout settings inside a container

meta and config are stored as JSON text; a value that is not valid JSON is 400. updatedAt is an ISO-8601 string with seven fractional digits; it is also the version token used for conflict detection.

Creating and updating​

POST /api/v1/views body {"name": "Overview", "description": "", "meta": "{}", "folder": "Line1"}; name is required (400 {"message":"name is required"}).

PATCH /api/v1/views/{id} takes the same fields, all optional; an empty name is 400.

PUT /api/v1/views/{id}/components takes a bare array of components and returns the stored array.

Saving with baseUpdatedAt​

PUT /api/v1/views/{id} is the designer's save:

FieldTypeRequiredMeaning
namestringno
metastringnoJSON text
componentsarrayyesThe full component list
baseUpdatedAtstringnoThe updatedAt the editor started from
forcebooleannoSave even if someone else saved since

When baseUpdatedAt is sent and differs from the stored updatedAt, and force is not true, nothing is written and the answer is:

409 { "statusCode": 409, "code": "CONFLICT", "message": "Someone else has saved this screen since you opened it.", "current": { "...the screen as it is now, with components" } }

On success the response is the screen with components and its new updatedAt.

Folders​

GET /api/v1/views/folders returns the folder paths. POST /api/v1/views/folders body {"path": "Line1/Pumps"}; .. is stripped, backslashes become slashes, leading and trailing slashes are removed. Response {"path": "Line1/Pumps"}.

Presence​

POST /api/v1/views/{id}/presence records that the signed-in person has the screen open and answers {"editors": [...]}, the other people who do. It needs a session, not an API key (400 otherwise). DELETE on the same path removes the caller. 404 {"message":"Screen not found"} for an unknown screen.

Start screens​

GET /api/v1/start-screens returns:

{
"entries": [ { "id": "…", "name": "Operators", "viewId": "…", "viewName": "Overview", "missing": false, "folder": "", "roles": ["operator", "engineer"], "isDefault": true, "order": 0 } ],
"defaultId": "…", "myChoice": null, "gatewayOpenOnLaunch": true, "myOpenOnLaunch": null, "openOnLaunch": true, "canManage": false
}

Without all=true the list holds only entries whose roles include the caller's role (an empty roles means everyone) and whose view still exists. all=true returns every entry and needs engineer (403 otherwise). defaultId is the caller's choice if still visible, else the gateway default, else the first entry. openOnLaunch is the caller's own setting when set, else the gateway's.

POST and PATCH take EntryBody:

FieldTypeMeaning
namestringRequired on create; up to 80 characters
viewIdstringRequired on create; must exist
folderstringGroup name, up to 120 characters
rolesstring[]Subset of viewer, operator, engineer, admin; listing all four is stored as empty (everyone)
isDefaultbooleanMakes this the one default. Unmarking the default hands it to the next in order; the only entry cannot be unmarked (400)

At most 100 start screens (400). The first entry created becomes the default and turns the gateway's Open start screen on launch setting on. Deleting the default hands it to the next entry.

PUT /api/v1/start-screens/order body {"ids": ["…", "…"]}: each id once; entries left out keep their place after the listed ones. Returns the ordered list.

PUT /api/v1/start-screens/me body {"startScreenId": "…", "openOnLaunch": "default" | "on" | "off"}, both optional. It belongs to a person, so it needs a session cookie, not an API key. An entry the caller may not see is 400. Returns the same shape as GET.