Appearance
Custom Page API (scadaink)
Last updated
Every custom page has a global scadaink object (also window.scadaink). It runs in the viewer's browser with the viewer's login: each call goes to the Scada.Ink REST API as that user, so the server applies that user's role. An operator's page can't do what an operator can't do. On a public page opened without logging in, calls that need a login fail, and live tags are limited to published ones (see Custom Page Security).
Unlike server scripts, most calls here return a Promise. Use .then() / .catch(), or await — top-level await works in the page's JavaScript. A script that uses it runs inside an async function, so its own functions are local: for an inline onclick="…" handler, assign the function to window (see Custom Pages). A failed call rejects with an Error whose message is the server's error, or HTTP 403 style text.
Device ids and names
Tag and history functions take a device's numeric id — the same id shown by Tag Snippet and used in tag:3/… shortcodes — or its name, such as 'CNC-001' (upper/lower case doesn't matter). A name is looked up once from the device list, which needs a signed-in viewer, so on a public page opened without logging in, use the numeric id. An unknown name logs scadaink.subscribe: Device not found: X to the browser console, and history calls reject with Device not found: X.
scadaink.read and scadaink.tags.readAll don't make a request, so with a name they only work once a subscribe call has looked it up.
Live tags
| Function | Returns | Notes |
|---|---|---|
scadaink.subscribe(deviceId, tagPath, callback) | nothing | callback(value, update): value is a string; update is { deviceId, tagPath, value, dataType, quality, timestamp }. The current value is sent immediately if the server has one, then on every change. |
scadaink.read(deviceId, tagPath) | string or undefined | Last value received by this page — undefined unless you've subscribed to that tag and a value has arrived. No request is made. |
scadaink.tags.subscribe / scadaink.tags.read | Same as above. | |
scadaink.tags.readAll(deviceId) | { [tagPath]: string } | Only the tags this page has received values for. |
scadaink.write(tagPath, value) | Promise | Writes a local tag. The tag must exist (Local tag not found). Roles: admin, editor, operator. |
scadaink.tags.write(tagPath, value) | Promise | Same as scadaink.write. |
scadaink.tags.writeDevice(deviceId, tagPath, value) | Promise | Writes a device tag. Roles: admin, editor, operator. The device must have writes enabled, and the tag's write security level applies. |
javascript
scadaink.subscribe(3, 'Spindle/Speed', function (value, update) {
document.getElementById('speed').textContent = Number(value).toFixed(0);
document.getElementById('speed').classList.toggle('stale', update.quality !== 'good');
});
document.getElementById('reset').addEventListener('click', function () {
scadaink.write('Line1/ResetRequest', 'true')
.then(function () { showMessage('Reset sent'); })
.catch(function (err) { showMessage('Reset failed: ' + err.message); });
});(showMessage stands for your own function that puts text on the page — alert() is blocked.)
Values are written as strings. There is no unsubscribe function; subscriptions last until the page is closed or reloaded.
Devices
| Function | Resolves to |
|---|---|
scadaink.devices.list() | The device list, as the Devices page gets it |
scadaink.devices.find(name) | Numeric id or null (name ignores case) |
scadaink.devices.localId() | Id of the built-in local device, or null |
Alarms
| Function | Resolves to |
|---|---|
scadaink.alarms.getActive() | Active alarms, as the Alarms page gets them |
scadaink.alarms.acknowledge(alarmId) | The API response; recorded as acknowledged by the viewer |
History
| Function | Resolves to |
|---|---|
scadaink.historian.query(deviceId, tagPath, start, stop?) | [{ timestamp, value }], oldest first, up to 1000 points |
scadaink.historian.queryAggregated(deviceId, tagPath, start, stop, window, fn) | [{ timestamp, value }], up to 2000 points; pass undefined for stop |
scadaink.history(deviceId, tagPath, start, stop?) | Same as historian.query |
start/stop accept relative times ('-1h', '-7d') or ISO timestamps, in UTC (…Z) or with an offset such as +02:00. fn is mean, max, min, sum, count, last or first. If the historian is unavailable the call rejects with InfluxDB is not available.
javascript
scadaink.historian.queryAggregated(3, 'Coolant/Temp', '-24h', undefined, '1h', 'max')
.then(function (points) {
points.forEach(function (p) { console.log(p.timestamp, Number(p.value)); });
});Data Tables
| Function | Resolves to | Roles |
|---|---|---|
scadaink.db.tables() | Array of local Data Table names. Remote tables are left out, because pages can't query them. | any signed-in user |
scadaink.db.query(table, { limit, offset, search, sortBy, sortDir }) | Array of rows | any signed-in user |
scadaink.db.insert(table, row) | The inserted row | admin, editor |
scadaink.db.update(table, rowId, changes) | The updated row | admin, editor |
scadaink.db.remove(table, rowId) / scadaink.db.delete(table, rowId) | The API response | admin, editor |
tableis the Data Table's display name. Unknown: Table not found.limitdefaults to 100 and is capped at 1000.sortDiris'asc'or'desc'(default'desc');sortBymust be a column,id,created_atorupdated_at— otherwise rows come newest first.searchmatches any column containing the text.- There is no
wherefilter here (unlike the server-sidedb.query); usesearch, or a named query throughscadaink.api.post.
Files
scadaink.fileUrl('images/logo.png') returns /api/files/serve/images/logo.png — the address of a file uploaded in the File Manager, with folder names as path segments (each segment URL-encoded). Use it for <img> sources, CSS backgrounds set from JavaScript, or fetch. The path is case-sensitive and must match the folder and file names exactly. File URLs don't need a login, so they also work on public pages; in HTML you can write the address directly: <img src="/api/files/serve/images/logo.png">. See File Manager.
scadaink.pageFile('logo.png') returns /api/page-files/serve/<id>/logo.png — the address of a file uploaded in this page's Page files panel, by its name, for the page the code is running on. It works on private pages for a signed-in viewer, and on public pages for anyone (see Custom Pages).
javascript
document.getElementById('logo').src = scadaink.pageFile('logo.png');Users (admin viewers only)
For any other role the server answers 403 and the Promise rejects.
| Function | Does |
|---|---|
scadaink.users.list() | Resolves to the user list |
scadaink.users.add({ email, password, firstName, lastName, role }) | Creates a user |
scadaink.users.update(id, changes) | Edits a user |
scadaink.users.setPassword(id, newPassword) | Sets a password |
scadaink.users.disable(id) / enable(id) | Blocks / allows sign-in |
scadaink.users.remove(id) | Deletes the user |
Any other endpoint
scadaink.api.get(path), .post(path, body), .put(path, body) and .del(path) call /api + path with the viewer's token, and resolve to the whole JSON response ({ success, data, … }). They reject when the HTTP status isn't OK or success is false.
javascript
scadaink.api.post('/named-queries/downtime_by_reason/execute', { params: { line: 2, start: '2026-09-14T06:00:00Z' } })
.then(function (res) { renderRows(res.data.rows); });Login helpers (for a custom sign-in page)
| Function | Does |
|---|---|
scadaink.auth.login(email, password) | Resolves to { user, tokens, passwordChangeRecommended }; rejects with the server's message (e.g. invalid credentials) |
scadaink.auth.applySession(session, redirectTo?) | Stores the session in this browser so the Scada.Ink app is signed in, then goes to redirectTo |
scadaink.auth.logout(redirectTo?) | Clears the stored session and goes to redirectTo (default /login) |
scadaink.auth.currentUser() | The stored user object, or null |
javascript
async function signIn() {
var email = document.getElementById('email').value;
var password = document.getElementById('password').value;
try {
var session = await scadaink.auth.login(email, password);
scadaink.auth.applySession(session, '/');
} catch (err) {
document.getElementById('error').textContent = err.message;
}
}
document.getElementById('signin').addEventListener('click', signIn);Note: Inside the app or at
/view/page/<id>the page runs in a sandboxed frame that isn't allowed to navigate the whole window, so the redirect loads the app inside the frame. For a sign-in screen, mark the page Public and open its raw address/api/custom-pages/<id>/renderdirectly (bookmark it or set it as the kiosk start page) — there the redirect replaces the whole window. Use a button with a click handler rather than a<form>submit.