API Reference
All PLC-scoped endpoints use the prefix /api/plcs/{plcId}/ where plcId is the configured alias (e.g., Line1, MyPLC).
Response Envelope
Every response uses a standard envelope:
// Success
{ "data": { ... }, "error": null }
// Error
{ "data": null, "error": { "code": "ADS_TIMEOUT", "message": "..." } }Error Codes
| Code | HTTP Status | Description |
|---|---|---|
SYMBOL_NOT_FOUND | 404 | PLC symbol does not exist |
PLC_NOT_FOUND | 404 | PLC alias not configured |
PLC_UNAVAILABLE | 503 | PLC is not connected |
ADS_TIMEOUT | 504 | ADS operation timed out |
ADS_ERROR | 502 | Generic ADS protocol error |
TYPE_MISMATCH | 400 | Value type doesn’t match what’s expected — e.g. a non-scalar (object/array) RPC parameter, see RPC |
BATCH_LIMIT_EXCEEDED | 400 | Batch request exceeds 100 symbols |
SUBSCRIPTION_LIMIT_EXCEEDED | 400 | Exceeded max symbols per client |
RATE_LIMITED | 429 | Lifecycle rate limit hit |
FEATURE_DISABLED | 404 (501 for Alarms, Rpc, TypeMetadata) | Feature slice is disabled — most slices answer 404; those three answer 501 so a fleet-aggregating gateway can tell “genuinely absent” from “transiently failing” |
UNAUTHORIZED | 401 | Missing or invalid JWT |
FORBIDDEN | 403 | Insufficient role or scope |
PLC_ACCESS_DENIED | 403 | plc_access claim denies this PLC |
MASTER_NOT_FOUND | 404 | EtherCAT master ID not found |
SLAVE_NOT_FOUND | 404 | EtherCAT slave address not found |
NOT_IMPLEMENTED | 501 | Endpoint not implemented by the underlying ADS library (all /files routes) |
Symbol Path Format
TwinCAT dot notation: MAIN.nCounter, MAIN.stMotor.bEnabled, GVL.arr[0]
Array indices with brackets must be URL-encoded in path segments: MAIN.arr%5B0%5D
System Endpoints
GET /api/plcs List all PLCs + connection status
GET /api/plcs/{plcId}/status Detailed status for one PLC
GET /health/live Liveness probe
GET /health/ready Readiness probe (PLC connectivity)
GET /health/alarms Worst outstanding alarm severity (when Alarms is enabled)
GET /openapi/v1.json OpenAPI spec
GET /swagger Scalar API UI (Development only)Health probes
The /health/* routes are the only anonymous surface adsify exposes — /api/plcs and
/api/plcs/{plcId}/status both require a token, which a load balancer or uptime checker
generally does not hold.
/health/live answers 200 Healthy for as long as the process runs. It consults no PLC, so it
stays a plain-text body.
/health/ready runs the checks that say whether adsify can serve what it was configured to
serve — plc-connectivity, plus ethercat-diagnostics when that feature is enabled — and
answers JSON naming each configured PLC:
{
"status": "Degraded",
"checks": [
{
"name": "plc-connectivity",
"status": "Degraded",
"description": "1/2 PLCs connected",
"data": { "Line1": "Connected", "Line2": "Disconnected" }
}
]
}| Condition | Status | HTTP |
|---|---|---|
| All configured PLCs connected | Healthy | 200 |
| Some connected, some not | Degraded | 200 |
| No PLCs connected, or none configured | Unhealthy | 503 |
| An EtherCAT master’s diagnostics are unreadable | Degraded | 200 |
When EtherCAT diagnostics are enabled, a second check reports whether adsify can still read each master — a different question from whether the PLC is connected, and the two come apart exactly when it matters: an ADS connection can be healthy while the master behind it has been unreadable for minutes.
{
"name": "ethercat-diagnostics",
"status": "Degraded",
"description": "1/2 EtherCAT masters have unreadable diagnostics",
"data": { "Line1/1": "Ok", "Line1/2": "DiagnosticsDegraded" }
}A master adsify cannot read at all reports DiagnosticsDegraded and degrades the check. One it
read but not completely reports IncompleteReads: … and does not — a host whose frame-counter
index group is unsupported reports frameStatistics incomplete on every cycle for the life of the
deployment, and that is not a fault. See EtherCAT for both signals.
A partial outage answers 200, deliberately. A host serving several targets can still serve
the ones that are up, so failing the probe over one unreachable line would pull the working PLCs
out of rotation too. Alert on the body — data names the PLC — rather than on the status code.
/health/ready never reflects alarm severity. See
Alarms for /health/alarms and why the two are separate.