Skip to main content

REST API (v1)

v1 REST API manual for PlantPulse Edge. This is the standard interface for screens and external systems to manage OPC, tags, monitoring, and external transmission health checks.

ItemValue
Base URLhttp(s)://<edge-host>/api/v1
Content-Typeapplication/json; charset=utf-8
AuthenticationAPI Key Mandatory (edge.rest.api.auth=true) — X-API-Key / Authorization: Bearer, or pass via login session. ?api_key= is rejected with 401
Audit LogApiAccessLogFilter — Every /api/* call is recorded in one line (Method/Path/Status/Duration/IP/User/Auth Mode)
Main Controllerplantpulse.app.edge.api.v1.* (OpcAPI, TagAPI, AppAPI, SystemAPI, MonitoringAPI)
Error Envelope{data, meta, errors} — Same for filter/advice paths
HTTPSedge.rest.api.https_only=true by default. Plain HTTP client calls are rejected or follow Tomcat HTTPS redirect policy

Authentication (ApiAuthFilter)​

Enabled from app.properties to edge.rest.api.auth=true. The key is edge.rest.api.key and is injected via EDGE_REST_API_KEY_FILE or EDGE_REST_API_KEY in production.

Passes if any one of the following three is met:

A. Login Session Pass (Browser)​

Users already logged in via /login/form — If the session contains the _USER_LOGIN attribute (JSONObject), ApiAuthFilter performs chain.doFilter(). All /api/* called via ajax from screens (JSP) work without additional keys.

EDGE_REST_API_KEY="$(tr -d '\r\n' < /run/secrets/edge-rest-api-key)"
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf 'header = "X-API-Key: %s"\n' "$EDGE_REST_API_KEY" > "$cfg"
curl --config "$cfg" https://edge.example.com/api/v1/tag/TAG_LS_XBC_0001/value

C. Authorization: Bearer <key> Header​

EDGE_REST_API_KEY="$(tr -d '\r\n' < /run/secrets/edge-rest-api-key)"
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$EDGE_REST_API_KEY" > "$cfg"
curl --config "$cfg" https://edge.example.com/api/v1/tag/TAG_LS_XBC_0001/value

D. ?api_key=<key> Query Parameter — Rejected​

Since keys in URLs appear in plain text and can remain in access logs, proxy caches, or browser history, v1 rejects this with 401. Please use the header methods (B/C).

Failure Response​

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8

{
"data": null,
"meta": {
"timestamp": 1778069486934,
"status": "ERROR",
"http_status": 401,
"request_id": "3b89b8f1-0fc1-4ab4-b4eb-d12dc314d500"
},
"errors": [
{
"code": "UNAUTHORIZED",
"message": "Invalid or missing X-API-Key."
}
]
}

Key comparison uses constantTimeEquals() (timing-attack defense).

edge.rest.api.auth=false (Development / Temporary)​

When authentication is disabled, ApiAuthFilter immediately performs chain.doFilter() — allowing calls from external sources without a key. Always recommend true in production networks.


Audit Log (ApiAccessLogFilter)​

Records all /api/* calls as single-line logs — including authentication failures (401) to maintain an audit trail (Filter chain order: ApiAccessLogFilter → ApiAuthFilter).

Format​

[API_ACCESS] method=GET path=/api/v1/tag/TAG_MS_1000/value query= status=200 duration=12ms
ip=100.127.147.98 user=admin auth=session ua=Mozilla/5.0...
FieldMeaning
methodHTTP Method
pathRequest URI (excluding query)
queryQuery string — ?api_key=... parts that are rejected are masked as ***
statusHTTP response code
durationProcessing time (ms)
ipDefault is RemoteAddr. In reverse proxy deployments that are edge.rest.api.trust_x_forwarded_for=true, uses first IP X-Forwarded-For
useruser_id for session users / XXXX*** (len=16) (first 4 chars + length) for apikey / - for anonymous
authsession (browser login) / apikey (X-API-Key/Bearer) / deprecated_query (rejected ?api_key= attempt) / none (missing key or anonymous)
uaUser-Agent (cut off at … if over 80 chars)
errorAdded error=ClassName when an exception occurs

Log Level Branching​

StatusLevelIntent
5xx or throwableERRORServer error — immediate alarm
401, 403WARNAuthentication/Authorization denial — audit priority
Others (2xx, 3xx, general 4xx)INFORoutine calls

Utilization​

  • Identifying who is sending wrong keys — check 401 trails via grep '\[API_ACCESS\].*status=401'
  • 5xx regression tracking — grep '\[API_ACCESS\].*status=5' + error= fields
  • Call frequency / Aggregation by user — grouping by auth=apikey/session, analysis by IP
  • Response delay slow queries — extract lines with large duration=

Plaintext keys are never left in logs — identification is done via query masking + 4-character user field prefix. This balances traceability for breach analysis and prevention of plaintext exposure.

Filter chain order is critical

web.xml mapping order: ApiAccessLogFilter first, then ApiAuthFilter. Since AccessLog records the status in the finally block, 401 calls that fail authentication are still audited. If the order is reversed, 401 calls will be missing from the access log.


Response Envelope​

v1 REST API responses are wrapped in a common envelope (RestApiSupport, ApiEnvelopeWriter).

Normal Response:

{
"data": { },
"meta": {
"timestamp": 1778069486934,
"status": "OK",
"request_id": "f818abd0-eb0f-4497-9b07-7ccc94b2e2d7"
}
}

Error Response:

{
"data": null,
"meta": {
"timestamp": 1778069486934,
"status": "ERROR",
"http_status": 400,
"request_id": "3b89b8f1-0fc1-4ab4-b4eb-d12dc314d500"
},
"errors": [
{
"code": "VALIDATION_FAILED",
"message": "Validation failed"
}
]
}

Old RPC-style envelopes (result, _session_id, _user_id) are not used in REST v1.


1. Common Conventions​

Before viewing endpoints, these are rules that all /api/v1/* follow together. Without understanding these, the client will be incorrect even when reading individual endpoint documentation.

1.1 Operational Headers​

HeaderDirectionPurpose
X-API-KeyRequestAuthentication. ?api_key= queries are deprecated(401)
Authorization: Bearer <key>RequestAlternative to API key
X-Request-IdRequestFor tracking. Echoed as response meta.request_id. If not provided, the server creates it
Idempotency-KeyRequestPOST duplication prevention. TTL 10 minutes. Retrying with the same key returns the cached response with Idempotency-Replay: true
If-MatchRequestOptimistic concurrency. 412 if different from ETag in GET response
ETagResponseweak validator(W/"<hex>"). Only sends GET /api/v1/opc/{opcId} currently
LocationResponseNew resource URI upon POST creation(201)
X-RateLimit-Limit · -Remaining · -ResetResponseRate limit information
Retry-AfterResponseSeconds to wait when 429

Rate limits are adjusted via edge.rest.api.rate_limit.enabled and edge.rest.api.rate_limit.per_second (default 1000).

Use Idempotency-Key if implementing retries

Field lines disconnect. A POST for which no response was received is not 「failed」 but 「unknown」 — simply resending may create duplicate OPCs. Sending with the same key will return the first result from the box.

1.2 Pagination​

Common queries for list endpoints.

QueryDefault ValueDescription
page0Page number starting from 0
size0Returns all if 0. If 1..1000, cuts the result accordingly and fills meta.pagination

If size exceeds 1000 or is negative, or if page is negative, it returns 400 INVALID_RANGE.

1.3 ID Rules​

TargetRegular Expression
site_id^SITE_[A-Z0-9_]{1,60}$
opc_id^OPC_[A-Z0-9_]{1,60}$
tag_id^TAG_[A-Z0-9_]{1,60}$

data_type only has six values: Boolean · Integer · Long · Float · Double · String (matching Cassandra ENUM). opc_type is a whitelist of protocol constants held by the driver (LS · OPCUA · MODBUS · MODBUS_RTU · HTTP · MELSEC · S7 · EIP …), and the official list is in GET /v3/api-docs. Values outside the whitelist are rejected as field_errors[].reason = "not_in_whitelist".


2. Endpoint List​

All 52 endpoints. The official OpenAPI spec is at GET /v3/api-docs in the box, and the screen view is at /public/swagger-ui/index.html.

2.1 OPC — 8​

MethodURLDescriptionResponse
GET/api/v1/opcOPC list + runtime status + 60min trend200 · 400
POST/api/v1/opcOPC registration. Tags can be included via tag_list201 · 400 · 409
GET/api/v1/opc/{opcId}Details + ETag. Tags array included via ?include=tags200 · 404
PUT/api/v1/opc/{opcId}Modification(If-Match supported)200 · 404 · 412
PATCH/api/v1/opc/{opcId}Partial modification — changes only sent fields200 · 400 · 404 · 412
DELETE/api/v1/opc/{opcId}Delete OPC + child tags200 · 404 · 412
POST/api/v1/opc/{opcId}/startStart collection200 · 404
POST/api/v1/opc/{opcId}/stopStop collection200 · 404

7 Mandatory POST Fields — site_id · opc_id · opc_name · opc_type · opc_agent_ip · opc_agent_port (1~65535) · timecycle (100ms or more). Violations are returned all at once (field_errors[]).

Register tags together using tag_list. Since it is common to put dozens of tags in one connection, splitting registration into two steps may leave an intermediate state where the OPC exists but tags are missing. Send them at once — alternatively, send the OPC first and attach tags one by one via POST /api/v1/opc/{opcId}/tag.

2.2 Tag — 10​

MethodURLDescriptionResponse
GET/api/v1/tagTotal tag list200 · 400
GET/api/v1/opc/{opcId}/tagTag list for a single OPC200 · 400
POST/api/v1/opc/{opcId}/tagSingle tag registration201 · 400 · 409
GET/api/v1/tag/{tagId}Tag details200 · 404
PUT/api/v1/tag/{tagId}Tag modification200 · 404
DELETE/api/v1/tag/{tagId}Tag deletion200 · 404
GET/api/v1/tag/{tagId}/valueLast value. If ?fresh=true, reads newly from device200 · 404 · 503
POST/api/v1/tag/{tagId}/valueWrite value + read back after writing(actual_read)200 · 400 · 404 · 503
POST/api/v1/tag-values:readCache values of multiple tags at once(max 100)200 · 400
GET/api/v1/tag/{tagId}/pointsTime series query + downsampling200 · 400 · 500

6 Mandatory POST Fields — site_id · opc_id (automatic from path) · tag_id · tag_name · data_type · plc_address.

Queries for points: from·to (epoch ms, from < to) · minutes (default 10, 11440) · limit (default 600, 15000 — if exceeded, thins out at equal intervals).

Write failure code mapping — Tag missing 404 TAG_NOT_FOUND · Connection lost 503 DISCONNECTED · Collection stopped 503 OPC_STOPPED · Read-only tag 400 RW_VIOLATION · Type mismatch 400 TYPE_MISMATCH · Others 500 INTERNAL_ERROR.

2.3 App (Docker Container) — 9​

MethodURLDescriptionResponse
GET/api/v1/appContainer list200 · 400
POST/api/v1/appPull image then run201 · 400 · 409 · 502
GET/api/v1/app/{name}docker inspect result200 · 404
DELETE/api/v1/app/{name}Deletion(include image if ?image=true)200 · 404
POST/api/v1/app/{name}/startStart200 · 404
POST/api/v1/app/{name}/stopStop200 · 404
POST/api/v1/app/{name}/restartRestart200 · 404
GET/api/v1/app/{name}/logs?lines=NLast N lines (1~5000, default 200)200 · 404
GET/api/v1/app/{name}/statsCPU · Memory · Network · Disk snapshot200 · 404

name and image must pass regular expression + shell metacharacter blocking (; & | \ $ ( ) < > \ " ' space). memory_limit_mb는 16 이상,cpu_limit은 0.1 이상,network는bridge·host·none` is.

Deployment failures specify the stage — pull failure 502 DOCKER_PULL_FAILED, run failure 502 DOCKER_RUN_FAILED, existing name 409 CONTAINER_EXISTS.

2.4 System — 12​

MethodURLDescriptionAuthentication
GET/api/v1/system/healthLiveness + 8 types of components checkNot required
GET/api/v1/system/readyReadiness (monitor + V5 client)Not required
GET/api/v1/system/versionVersion · Build date · Image tag · Container statusNot required
GET/api/v1/system/timeCurrent box time (epoch ms)Required
POST/api/v1/system/time/syncSync to platform time ({"platform_time":…})Required
GET/api/v1/system/logsRecent logs as JSONRequired
GET/api/v1/system/logs/downloadLog directory via tar.gzRequired
GET/api/v1/system/eventsSSE Stream (text/event-stream)Required
GET/api/v1/system/upgrade/checkCompare with latest version of download serverRequired
POST/api/v1/system/upgradeUpgrade triggerRequired
GET/api/v1/system/upgrade/statusUpgrade progress (polling)Required
POST/api/v1/system/firmwareFirmware update triggerRequired
POST/api/v1/system/rebootHost reboot after 5 secondsMandatory
POST/api/v1/system/restartEdge agent restart after 3 secondsMandatory
Authentication for reboot · restart «cannot be disabled»

Even if authentication is disabled via edge.rest.api.auth=false, these two require session login or an API key. If anyone could call these, it would be a denial of service in itself.

2.5 Monitoring · Edge — 4​

MethodURLDescription
GET/api/v1/monitoringUptime · OPC count · Registered tags · Scan speed
GET/api/v1/edgeedge_id · site_id · Version · Hostname · Mode
GET/api/v1/edge/statusEdge status summary
GET/api/v1/transferStatus by outbound transmission (MQTT · SparkplugB · Platform API)

monitoring returns 503 MONITOR_NOT_READY if the monitor is not yet available immediately after booting — this is not a failure but 「not yet」.

2.6 Redundancy (HA) — 4​

MethodURLDescription
GET/api/v1/ha/statusThis edge's write authority · Mode (manual/auto) · Since when · Opponent box status
PUT/api/v1/ha/write-authorityRaise or lower write authority for this edge ({"enabled":true})
PUT/api/v1/ha/failover/overrideManual intervention in auto failover — Only in auto mode (otherwise 409 NOT_AUTO_MODE)
POST/api/v1/ha/definitionsReplicate tag definition to opponent

Write Authority — Things to know before reading​

What does it block? An edge without authority rejects actual PLC writes (WRITE_NOT_AUTHORIZED). The configuration key is edge.plc.write_authority and if absent, it is true — a newly installed box starts with authority.

HTTP collection does not pass through this gate — but not «always»

A connection where opc_type is HTTP is incoming collection, not a write sent to the device. Therefore, HTTP collection continues even on an edge with lowered authority — as there is no risk of double writing.

⚠ Immediately after booting, that HTTP write may be briefly rejected. The gate reads opc_type from the cache; if the cache is not yet filled or the lookup fails, it treats it as «actual PLC» and blocks it (intended behavior to fail safe). Furthermore, that lookup failure does not leave a log.

So if you encounter 「HTTP was supposed to continue but was rejected」, first check if it is immediately after startup. It will resolve itself once the cache is filled — it is not a matter of fixing settings.

🔴 This API does not «transfer» — it is two independent calls: raise and lower

This call only changes its own box. It does not ask, lock, or receive from the opponent. There is no coordination code.

Therefore, it is actually possible for two boxes to have authority simultaneously. Follow the sequence:

  1. Lower A — PUT /api/v1/ha/write-authority {"enabled":false}
  2. Verify A is false — GET /api/v1/ha/status
  3. Raise B

If done in reverse, a window where both are true is created, and both boxes write to the same PLC during that window.

The peer.write_authority of GET /api/v1/ha/status shows the opponent's value. However, that is observation, not enforcement — this call is not blocked even if the opponent has authority.

persisted:false is not 「failure」 but 「disappears upon restart」

Toggling is a two-step process. ① Live application — the gate immediately sees the new value (no restart required). ② app.properties persistence — even if this fails, ① is maintained and reports as persisted:false.

In other words, it works now but will return to the old value on the next restart. This is where most field errors occur — restarting after believing authority was lowered, only to find it raised again. Resolve the cause before restarting — usually write authority in /etc/kopens/conf. Diagnosis is in the server log, not the response:

[HA] write-authority persist to app.properties failed — applied live only

since is the 「last time control was transferred」. It is updated only when the value actually changes, so it does not move if the same value is sent again.

Audits are always left — WARN if changed, INFO with [no-op] if unchanged; both include the caller's IP and account.

[HA][AUDIT] write-authority false -> true by 10.0.0.5 (admin)

⚠ Transition history and platform diagnosis pushes occur only when values change, and the toggle is not broken even if those two fail (they only remain in logs). 「Not in history」 does not mean 「not changed」 — verify with status.

Two things the edge informs on its own. The side monitoring the pair leaves the mismatched state in the log. It does not fix the settings — the operator does.

[HA] DUAL WRITE AUTHORITY detected — both edges have edge.plc.write_authority=true.
[HA] NO WRITE AUTHORITY — both edges have edge.plc.write_authority=false.

The latter is a state where PLC writes are disabled everywhere — the case where 「both were lowered and raising was forgotten」.

Failover Manual Intervention — PUT /api/v1/ha/failover/override​

Stops automatic failover from making decisions about this edge and fixes the role. Auto-decision returns only after being released.

roleMeaning
"WRITER"Keep writing heartbeat and hold authority
"STANDBY"Stop writing and release authority
nullRelease intervention — return to auto-decision
{ "data": { "override": "WRITER", "previous": null }, "meta": { "status": "OK" } }
  • Exclusive to auto mode. If auto failover is not running, it returns 409 NOT_AUTO_MODE — since the preceding write-authority works in both modes, use that if in manual operation.
  • WRITER·STANDBY are case-insensitive.
  • If the value is not one of the three, 400 VALIDATION_FAILED with field_errors:[{field:"role", reason:"invalid_value"}] is returned.

Time Synchronization — POST /api/v1/system/time/sync​

{ "platform_time": 1789780169674 }

Epoch milliseconds (positive) and accepts either numbers or numeric strings. If platform_time is omitted, it syncs from the NTP source configured in the box — sending a value and omitting it are different behaviors.

The response is in the form of {"applied":true,"method":"system","before":…,"after":…}. Comparing before/after reveals how much it was actually offset.

Authentication comes «before verification»

Since this call changes the system, authentication is checked before the body. 401 if key or session is missing, and 400 VALIDATION_FAILED if the body is wrong after authentication. Do not confuse this with APIs where the order is reversed — if 401 is returned for a wrong body, it is not a body issue.

2.7 Backup · Recovery — 2​

MethodURLDescription
POST/api/v1/connect/backupAll OPCs + all tags into one JSON
POST/api/v1/connect/restoreApply that JSON as-is (upsert)

Symmetric — simply send the backup response as the restore body. Always check skipped[] in the restore response: even if individual rows fail, the whole process does not fail; it skips the row and only decreases the count.

2.8 Formula — 1​

MethodURLDescription
POST/api/v1/tags/formula/previewPreview result before saving the formula — Formula

2.9 Unauthenticated Network​

Only this network passes through ApiAuthFilter. It does not export any OPC or tag information.

GET /api/v1/system/health · GET /api/v1/system/ready · GET /api/v1/system/version · GET /api/health (alias without version)

«Authentication not required» means the key cannot be verified

This network returns 200 even if X-API-Key is missing or incorrect. Therefore, this path should not be used to verify if a key was changed correctly — verify using paths that require a key, such as GET /api/v1/opc.

/health checks eight slots. If all eight are UP, it returns status=UP + 200; otherwise, status=DEGRADED + 503.

{
"data": {
"status": "UP",
"uptime_ms": 1720008,
"components": {
"cassandra": "UP", "redis": "UP", "mqtt": "UP", "node_red": "UP",
"opc_ua": "UP", "edge_core": "UP", "tse": "UP", "grafana": "UP"
}
},
"meta": { "timestamp": 1778925572946, "request_id": "..." }
}

🔴 components is still included even in 503. That is the entirety of the diagnosis — if the body is discarded by looking only at the status code, there is no way to know which of the eight died.

Three slots are listed separately as they are easily misunderstood by name.

SlotWhat is actually seen
edge_corereadiness — whether the collector/monitor has started. If in a booting race, it is DOWN, making health 503. It is independent of platform connection, so it is meaningful even in a STANDALONE box
opc_uaPort is not fixed. It follows opc.ua.server.tcp.port of app.properties (4840 for dev boxes, 12000 by default). There was a case where it was hardcoded to 12000, causing dev boxes to be reported as DOWN forever
grafanaFrom image 2026-07, it is HTTPS. Poking with plain TCP causes Grafana to leave TLS handshake error … EOF in logs every 10 seconds — therefore, the box first uses a probe that completes the handshake, then falls back to TCP checks for older stacks

cassandra(9042) · redis(6379) · mqtt(1883) · node_red(1880) · tse(7800) are TCP checks for fixed ports.

/version example:

{
"data": {
"product_name": "PlantPulse Edge",
"version": "2026",
"build_date": "20260523",
"image_tag": "2026-20260523",
"container_mode": true
},
"meta": { "timestamp": 1778925572946, "request_id": "..." }
}

OTA upgrade.sh pokes /api/health for 300 seconds to determine automatic rollback, and Docker HEALTHCHECK also uses this path.

2.10 Non-API Paths — /ui/opcua/* · /config/*​

The box contains paths used by the screen. New clients must not use these.

  • /ui/opcua/info · /tree · /history — Exclusive to the built-in OPC UA server screen. The official v1 API of the product has declared these deprecated. The code still responds, but it is not a contract.
  • /config/load · /save · /restart · /reboot · /upgrade · /firmware · /backup · /temp-clean · /platform/test — Paths used by the admin screen. Since /api/v1/system/* performs the same task, use that instead — it follows v1 conventions for envelopes and error codes.
  • /api/http(APIController) — Deprecated. Use POST /api/v1/tag/{tagId}/value.

3. Request / Response Examples​

All examples below use the current envelope. Examples in the shape of {"result":"OK", ...} found in old documents have been removed as they do not match the product — clients designed with that shape will not work now.

3.1 OPC Registration​

curl -sk -X POST "https://<엣지>/api/v1/opc" \
-H "X-API-Key: <key>" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"site_id": "SITE_00001",
"opc_id": "OPC_LS_XBM_0001",
"opc_name": "1라인 LS XBM",
"opc_type": "LS",
"opc_agent_ip": "192.168.0.10",
"opc_agent_port": 2004,
"timecycle": 1000,
"tag_list": [
{ "site_id": "SITE_00001", "opc_id": "OPC_LS_XBM_0001",
"tag_id": "TAG_LS_XBM_0001", "tag_name": "1호기 토출압력",
"data_type": "Float", "plc_address": "%MW100" }
]
}'

tag_list can be omitted — in which case, only the OPC is created.

201 + Location: /api/v1/opc/OPC_LS_XBM_0001:

{
"data": { "info": { "opc_id": "OPC_LS_XBM_0001", "opc_name": "1라인 LS XBM" } },
"meta": { "timestamp": 1778925572946, "status": "OK", "request_id": "..." }
}
opc_agent_port and timecycle can be sent as numbers

Since the corresponding columns in storage are strings, the server converts them upon receipt. It is not an error to send as a number and receive back as a string.

3.2 Deletion​

{
"data": { "deleted": true, "opc_id": "OPC_X" },
"meta": { "timestamp": 1778925572946, "status": "OK", "request_id": "..." }
}

DELETE also returns 200 + body. Even if some Swagger comments state 204, the actual behavior is 200 — all methods in v1 return an envelope.

3.3 Failure Responses​

POST /api/v1/opc/OPC_NONE/start — 404:

{
"meta": { "timestamp": 1778925572946, "status": "ERROR", "request_id": "..." },
"errors": [ { "code": "OPC_NOT_FOUND", "message": "OPC OPC_NONE 가 존재하지 않습니다." } ]
}

data is entirely absent (null field is omitted during serialization). Do not distinguish success from failure by the presence of data, but use the HTTP status or meta.status.

3.4 Multiple Tag Values at Once​

curl -sk -X POST "https://<엣지>/api/v1/tag-values:read" \
-H "X-API-Key: <key>" -H "Content-Type: application/json" \
-d '{ "tag_ids": ["TAG_LS_XBM_0001", "TAG_LS_XBM_0002"] }'
{
"data": [
{ "tag_id": "TAG_LS_XBM_0001", "value": "1234", "timestamp": "2026-05-15T11:32:45.000Z", "quality": "SUCCESS" },
{ "tag_id": "TAG_LS_XBM_0002", "value": null, "timestamp": null, "quality": "PENDING" }
],
"meta": { "timestamp": 1778925572946, "status": "OK", "request_id": "..." }
}

🔴 Look at quality. PENDING means 「has not been read yet」, not 「value is 0」. Reading this as 0 would result in equipment that no one has looked at being reported as 「normal」.

If the array is empty or exceeds 100 items, it returns 400(MISSING_FIELD / TOO_MANY).

3.5 Restart​

{
"data": { "message": "reboot scheduled in 5 seconds" },
"meta": { "timestamp": 1778925572946, "status": "OK", "request_id": "..." }
}

202 — meaning 「decided to do」, not 「done」. The system is still alive at the moment the response is received.

3.6 Not Ready Yet​

GET /api/v1/system/ready — 503:

{
"meta": { "timestamp": 1778925572946, "status": "ERROR", "request_id": "..." },
"errors": [ { "code": "NOT_READY", "message": "엣지의 일부 구성요소가 준비되지 않았습니다." } ]
}

3.7 Backup and Recovery are Symmetric​

{
"data": {
"opcs": [ { "opc_id": "OPC_LS_XBM_0001", "opc_name": "..." } ],
"tags": [ { "tag_id": "TAG_LS_XBM_0001", "opc_id": "OPC_LS_XBM_0001" } ],
"count": { "opc": 1, "tag": 1 },
"exported_at": 1778925572946
},
"meta": { "timestamp": 1778925572946, "status": "OK", "request_id": "..." }
}

Simply send data as the body for POST /api/v1/connect/restore. Always check skipped[] in the recovery response — even if individual rows fail, the whole process is 200, and only the skipped items are listed there. Looking only at count may lead to silent partial recovery.


4. Error Codes​

🔴 v1 communicates failure via HTTP status. The old RPC method (wrapping {result:"ERROR"} in HTTP 200) is deprecated. Clients that look for result in the body without checking the status code will read all failures as successes.

Determine the outcome using one of two ways — HTTP status code, or meta.status("OK" / "ERROR"). It is split by a single key without needing to search for errors[].

codeHTTPMeaning
MISSING_BODY400Request body is empty
MISSING_FIELD400Mandatory field missing
INVALID_JSON400JSON parsing failed
INVALID_RANGE400Range violation for page·size·from~to
INVALID_ARGUMENT400General argument error (image/volume whitelist, etc.)
INVALID_NAME400Injection characters in container name
VALIDATION_FAILED400Multiple fields are wrong — see field_errors[]
TOO_MANY400Batch request limit (100 items) exceeded
RW_VIOLATION · TYPE_MISMATCH400Tag write failure — read-only tag / type mismatch
UNAUTHORIZED401Authentication missing or invalid
HTTPS_REQUIRED403Arrived via plain HTTP — not a key issue
OPC_NOT_FOUND · TAG_NOT_FOUND · APP_NOT_FOUND404Target not found
CONTAINER_EXISTS409Container with the same name already exists
PRECONDITION_FAILED412ETag for If-Match differs — someone modified it in between
START_FAILED · STOP_FAILED422OPC collection control failed
DOCKER_PULL_FAILED · DOCKER_RUN_FAILED502Docker command failed
MONITOR_NOT_READY · NOT_READY · DISCONNECTED · OPC_STOPPED503Not yet/Disconnected — not a request error
MISCONFIGURED500edge.rest.api.key is empty
INTERNAL_ERROR · *_FAILED500Other internal errors
Do not treat 503 like 400

DISCONNECTED · OPC_STOPPED · MONITOR_NOT_READY occur not because the request is wrong, but because it cannot be done now. You can send the same request again later. If handled as 400 「fix and retry」, you will try to fix something that is not broken.


5. Operational Notes​

  • Single Tag Query: R03 of APP_TAG.xml (WHERE TAG_ID = :tag_id ALLOW FILTERING).
  • After OPC registration · Tag registration, ConnectService.restartComponents() returns to re-read the cache and restart the OPC-UA server — this is why collection briefly disconnects immediately after registration.
  • Write Value (tagWriteByTagId): HTTP is HTTPDriver.bind(), others are driver.write(ProtocolAddress). In case of failure (ok=false), includes helpful hints (EIP firmware compatibility, etc.). The result of an immediate read after writing is returned together as actual_read (except for HTTP).
  • Transfer Health (/api/v1/transfer): Collects getStatus() of all outbound components registered in TransferRegistry. Sparkplug B card (/ui/main) calls this every 5 seconds.
  • OPC UA Server (/ui/opcua/*) performs internal network polling (2s) — low load as it uses only OPCAndTagsCache.getInstance() + LastValueMap.getInstance().
  • Legacy deprecated: /api/http (APIController) — use /api/v1/tag/{tagId}/value.

6. Code Locations​

  • Controllers: src/plantpulse/app/edge/api/v1/ — OpcAPI · TagAPI · AppAPI · SystemAPI · SystemTimeAPI · MonitoringAPI · EdgeStatusAPI · HaAPI · ConnectIoAPI · FormulaAPI
  • MCP Server: src/plantpulse/app/edge/module/mcp/ — MCP Server
  • envelope · Common response: api/v1/RestApiSupport.java · api/v1/dto/Api*.java
  • OPC UA Server: src/plantpulse/app/edge/module/opcua/OPCUAViewerController.java
  • Upgrade Proxy: src/plantpulse/app/edge/module/system/config/ConfigController.java
  • Business Logic: src/plantpulse/app/edge/module/connect/ConnectService.java
  • Response envelope helper: plantpulse.app.edge.core.web.ControllerSupport (ok() / error())