Skip to main content
Version: 0.1

REST API

The gateway serves a JSON API under /api/v1, the same API the dashboard uses. This section is the reference for it: every route, its role, its request and response shapes. Live data (tag subscriptions, replay, alarm frames) goes over the WebSocket described in Live data over /sync; the TypeScript SDK wraps both.

Base URL​

All routes in this section are relative to the gateway's public address, for example https://app.scadex.io. Resource routes live under /api/v1/...; authentication routes live under /api/auth/.... Module routes live under /api/v1/modules/{moduleId}/....

Authentication​

Two ways to authenticate; both produce the same principal with a role.

MethodHowLimits
Session cookiePOST /api/auth/sign-in/email sets the scadex.session cookie. The cookie is HTTP-only, valid 7 days with no sliding renewal, and Secure when AUTH_BASE_URL starts with https://The session row in the database is checked on every request, so sign-out and role changes apply to the next request
API keyAuthorization: Bearer scx_...A key holds a role of viewer, operator or engineer; admin keys cannot exist. The key is hashed at rest; lastUsedAt is refreshed at most once a minute

A request that carries a Bearer scx_ header is authenticated by the key, otherwise by the cookie. A request with neither gets 401. Every route needs at least the viewer role unless it is marked anonymous; an endpoint that declares no policy is closed, not open.

A few routes are about the signed-in person and need the cookie, not a key: GET /api/v1/me, /api/v1/preferences, PUT /api/v1/start-screens/me, creating API keys and the screen presence routes. The /sync upgrade always needs a trusted Origin header, whichever credential it carries, and answers 403 without one.

See Authentication for the sign-in, sign-up, session and OIDC routes, and API keys for managing keys.

The Origin requirement​

The gateway has no CSRF token. Instead, every request that is not GET, HEAD or OPTIONS and carries a Cookie header must send an Origin (or, failing that, Referer) header that matches one of the origins in AUTH_TRUSTED_ORIGINS (comma-separated; https://*.example.com wildcard patterns are allowed). Browsers send Origin on their own. From curl or a script using a session cookie, pass it yourself:

curl -b cookies.txt -H "Origin: https://your-host" -H "Content-Type: application/json" ...

A failing check answers 403 with {"code":"MISSING_OR_NULL_ORIGIN","message":"Forbidden"} or {"code":"INVALID_ORIGIN","message":"Forbidden"}. The check runs before authentication. An empty AUTH_TRUSTED_ORIGINS means no origin is trusted, so every cookie-authenticated write gets 403. Requests authenticated with an API key and no cookie are not subject to the check.

Roles​

A user has one role. Each role can do everything the one before it can.

RoleAdds
viewerRead everything below /api/v1 except users and the audit log; subscribe and replay on /sync; change own preferences
operatorWrite to tags (POST .../tags/{id}/write, tag.write on /sync); acknowledge and shelve alarms; restart tags
engineerConfigure tags, tag types, screens and start screens; call module routes that change something (driver connections, browsing)
adminManage users, API keys, identity providers and settings; read the audit log; install and remove modules; backup and restore

Module routes have no say in their policy: a GET needs viewer, any other method needs engineer, unless the module marked the endpoint as writing tag values, which lets operators call it. More in Users and roles.

The role is read from the database on every request, not from the cookie. An insufficient role answers 403 with {"statusCode":403,"code":"FORBIDDEN","message":"Forbidden"}.

JSON conventions​

  • Request and response bodies are JSON with camelCase property names. Send Content-Type: application/json.
  • Enumerations and states are lowercase strings ("good", "running", "viewer").
  • Absent and null mean the same thing in most bodies; where an explicit null differs from absent (for example memoryValue on a tag update), the page says so.
  • Tag values are plain JSON values (number, boolean, string); NaN and Infinity are written as the literals "NaN", "Infinity" and "-Infinity".

Timestamps​

Two forms are in use. Each page says which a field uses.

  • ISO-8601 strings with offset ("2026-11-08T12:00:00.000+00:00"): entity fields such as createdAt, updatedAt, expiresAt, tag value timestamps and the timestamp of the runtime replay route.
  • Unix milliseconds (integers): alarm history (from, to, before, nextBefore), alarm shelving (durationMs), screen baseUpdatedAt, and module log paging.

Errors​

Errors are JSON. The body shape depends on where the error is raised.

StatusBodyWhen
400{"statusCode":400,"message":"..."}Validation the gateway does itself (missing field, bad value, read-only built-in type)
400ASP.NET validation problem details ({"title":"One or more validation errors occurred.","status":400,"errors":{...}})The body could not be bound to the request record
401{"statusCode":401,"code":"UNAUTHORIZED","message":"Unauthorized"}No valid session or key
401{"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"}Sign-in failed
403{"statusCode":403,"code":"FORBIDDEN","message":"Forbidden"}Role too low
403{"code":"MISSING_OR_NULL_ORIGIN"|"INVALID_ORIGIN","message":"Forbidden"}Origin check failed
404{"statusCode":404,"message":"..."}Unknown id or path
409{"statusCode":409,"message":"..."}Conflict: a tag path or user email already exists, the last admin would be removed, a screen was changed by someone else
503Problem details {"title":"Module unavailable","status":503,"detail":"Module 'x' is not running (stopped)."}A module route whose module is stopped, starting, stopping or failed

A route under /api/v1/modules/{moduleId}/... answers 404 when the module was never loaded (disabled, unlicensed, refused, or not installed) and 503 when it is installed but not running. That distinction is deliberate: 404 means "no such module", 503 means "not now".

Some routes return a result object with 200/202 on success and the same object with 400 on refusal (tag writes, for instance). Those pages show the shape.

Paging​

Lists that can grow without bound page backwards in time with a cursor:

  • Pass limit for the page size (each route has its own default and maximum).
  • The response carries items (or a named array) and nextBefore. Pass nextBefore as before on the next request to get the older page. A null nextBefore means there is no more.
  • Alarm history uses Unix milliseconds for before/nextBefore; module logs and the audit log use the value the previous response returned. Pass it back unchanged.

Routes that do not page return the whole collection.

A walkthrough with curl​

Sign in and keep the cookie:

curl -c cookies.txt -H "Content-Type: application/json" \
-H "Origin: https://app.scadex.io" \
-d '{"email":"you@example.com","password":"..."}' \
https://app.scadex.io/api/auth/sign-in/email

List tags under a folder:

curl -b cookies.txt "https://app.scadex.io/api/v1/tags?parentPath=Plant.Line1"

Read a tag's current value, by path or id:

curl -b cookies.txt "https://app.scadex.io/api/v1/runtime/tags/Plant.Line1.Setpoint/current"

Write a value (operator or higher; Origin is required because the request carries the cookie):

curl -b cookies.txt -H "Content-Type: application/json" \
-H "Origin: https://app.scadex.io" \
-d '{"value": 42}' \
https://app.scadex.io/api/v1/runtime/tags/Plant.Line1.Setpoint/write

The same write with an API key needs no cookie and no Origin:

curl -H "Authorization: Bearer scx_..." -H "Content-Type: application/json" \
-d '{"value": 42}' \
https://app.scadex.io/api/v1/runtime/tags/Plant.Line1.Setpoint/write

Pages​

PageCovers
AuthenticationSign-in, sign-up, session, sign-out, OIDC login
TagsTag CRUD, move, copy, import and export, current value, write, inject
Tag typesTypes, members, parameters, folders, instances
AlarmsDefinitions, active list, acknowledge, shelve, history
ScreensViews, folders, start screens, presence
Runtime and modulesHealth, module lifecycle, logs, driver routes
AdministrationUsers, API keys, backup, identity providers, modes, settings, preferences
ConnectionsDatabase connections and storage, driver connections
HistoryHistorian module routes, reports
Other routesEvent streams, alarm pipelines, clients, cron module
Live data over /syncThe WebSocket protocol
TypeScript SDKThe client library