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.
| Method | How | Limits |
|---|---|---|
| Session cookie | POST /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 key | Authorization: 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.
| Role | Adds |
|---|---|
viewer | Read everything below /api/v1 except users and the audit log; subscribe and replay on /sync; change own preferences |
operator | Write to tags (POST .../tags/{id}/write, tag.write on /sync); acknowledge and shelve alarms; restart tags |
engineer | Configure tags, tag types, screens and start screens; call module routes that change something (driver connections, browsing) |
admin | Manage 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
nullmean the same thing in most bodies; where an explicitnulldiffers from absent (for examplememoryValueon a tag update), the page says so. - Tag values are plain JSON values (number, boolean, string);
NaNandInfinityare 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 ascreatedAt,updatedAt,expiresAt, tag value timestamps and thetimestampof the runtime replay route. - Unix milliseconds (integers): alarm history (
from,to,before,nextBefore), alarm shelving (durationMs), screenbaseUpdatedAt, and module log paging.
Errors
Errors are JSON. The body shape depends on where the error is raised.
| Status | Body | When |
|---|---|---|
400 | {"statusCode":400,"message":"..."} | Validation the gateway does itself (missing field, bad value, read-only built-in type) |
400 | ASP.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 |
503 | Problem 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
limitfor the page size (each route has its own default and maximum). - The response carries
items(or a named array) andnextBefore. PassnextBeforeasbeforeon the next request to get the older page. AnullnextBeforemeans 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
| Page | Covers |
|---|---|
| Authentication | Sign-in, sign-up, session, sign-out, OIDC login |
| Tags | Tag CRUD, move, copy, import and export, current value, write, inject |
| Tag types | Types, members, parameters, folders, instances |
| Alarms | Definitions, active list, acknowledge, shelve, history |
| Screens | Views, folders, start screens, presence |
| Runtime and modules | Health, module lifecycle, logs, driver routes |
| Administration | Users, API keys, backup, identity providers, modes, settings, preferences |
| Connections | Database connections and storage, driver connections |
| History | Historian module routes, reports |
| Other routes | Event streams, alarm pipelines, clients, cron module |
| Live data over /sync | The WebSocket protocol |
| TypeScript SDK | The client library |