Skip to main content

REST API (v1)

The v1 REST API manual for PlantPulse Edge. This is the standard interface through which screens and external systems handle OPC, tags, monitoring, and outbound transfer health checks.

ItemValue
Base URLhttp(s)://<edge-host>/api/v1
Content-Typeapplication/json; charset=utf-8
AuthenticationAPI Key enforced (edge.rest.api.auth=true) — X-API-Key / Authorization: Bearer, or pass via a login session. ?api_key= is rejected with 401
Audit logApiAccessLogFilter — records every /api/* call on a single 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} — identical on the filter/advice path
HTTPSedge.rest.api.https_only=true by default. Plain HTTP client calls are rejected or follow the Tomcat HTTPS redirect policy

Authentication (ApiAuthFilter)

Enabled with edge.rest.api.auth=true in app.properties. The key is edge.rest.api.key; in production, inject it via EDGE_REST_API_KEY_FILE or EDGE_REST_API_KEY.

A request passes if any one of the following three conditions is met:

A. Login session (browser)

A user already logged in via /login/form — if the session has the _USER_LOGIN attribute (JSONObject), ApiAuthFilter simply calls chain.doFilter(). Every /api/* invoked by ajax from the screens (JSP) works without an additional key.

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

Because the key would appear in plain text in the URL and could remain in access logs, proxy caches, or browser history, v1 rejects this with 401. 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 protection).

edge.rest.api.auth=false (development / temporary)

When authentication is disabled, ApiAuthFilter immediately calls chain.doFilter() — external callers can invoke the API without a key. Always use true on operational networks.


Audit log (ApiAccessLogFilter)

Records every /api/* call as a single log line — authentication failures (401) are included as well, leaving 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 (query excluded)
queryQuery string — the rejected ?api_key=... portion is also masked as ***
statusHTTP response code
durationProcessing time (ms)
ipRemoteAddr by default. Only in reverse proxy deployments where edge.rest.api.trust_x_forwarded_for=true is set, the first IP from X-Forwarded-For
useruser_id for a session user / XXXX*** (len=16) for apikey (first 4 characters + length) / - for anonymous
authsession (browser login) / apikey (X-API-Key/Bearer) / deprecated_query (rejected ?api_key= attempt) / none (missing key or anonymous)
uaUser-Agent (truncated with beyond 80 characters)
errorerror=ClassName appended when an exception occurs

Log level branching

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

Usage

  • Who is sending an invalid key — check the 401 trail with grep '\[API_ACCESS\].*status=401'
  • 5xx regression trackinggrep '\[API_ACCESS\].*status=5' + the error= field
  • Call frequency / per-user aggregation — group by auth=apikey/session, analyze by ip
  • Slow responses — extract lines with a large duration=

The plaintext key is never logged anywhere — query masking plus the 4-character prefix in the user field are enough for identification. This balances sufficient identifiability for incident analysis against plaintext exposure.

Filter chain order matters

web.xml mapping order: ApiAccessLogFilter first, then ApiAuthFilter. Because AccessLog records the status in a finally block, even 401 calls that fail authentication are audited. If the order is reversed, 401 calls are dropped 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"
}
]
}

The old RPC-style envelope (result, _session_id, _user_id) is not used in REST v1.


1. Endpoint list

1.1 OPC

SlugMethodURLDescription
flow_edge_opc_listGET/api/v1/opcOPC list (includes tag_count / connection_status / scan_status)
flow_edge_opc_createPOST/api/v1/opcRegister an OPC together with its tags
flow_edge_opc_updatePUT/api/v1/opc/{opcId}Update an OPC together with its tags (the path opcId overrides the body opc_id)
flow_edge_opc_deleteDELETE/api/v1/opc/{opcId}Delete an OPC and all of its tags at once
flow_edge_opc_startPOST/api/v1/opc/{opcId}/startStart collection
flow_edge_opc_stopPOST/api/v1/opc/{opcId}/stopStop collection

1.2 Tag

SlugMethodURLDescription
flow_edge_tag_listGET/api/v1/opc/{opcId}/tagTag list for a specific OPC (includes last value/status)
flow_edge_tag_createPOST/api/v1/opc/{opcId}/tagRegister a single tag
flow_edge_tag_updatePUT/api/v1/tag/{tagId}Partially update a single tag (merges with the existing row, then upserts)
flow_edge_tag_deleteDELETE/api/v1/tag/{tagId}Delete a single tag
flow_edge_tag_readGET/api/v1/tag/{tagId}/value[?fresh=true]Cached value (default); with fresh=true, reads directly from the PLC
flow_edge_tag_writePOST/api/v1/tag/{tagId}/valueWrite a value — all protocols supported (HTTP / OPC-UA / Modbus / MELSEC / S7 / LS / EIP)

1.3 Monitoring / Edge / Transfer

SlugMethodURLDescription
flow_edge_monitoringGET/api/v1/monitoringAll fields of MonitorBean: CPU/memory/network/disk/threads, etc.
flow_edge_infoGET/api/v1/edgeEdge identity/version/site/OS/uptime metadata
flow_edge_transferGET/api/v1/transferHealth status per outbound transfer (API/MQTT/Sparkplug)

1.3a System (no authentication required — for LB / k8s probes / OTA)

Among /api/v1/system/*, the three below pass the ApiAuthFilter whitelist (they expose no OPC/Tag information).

MethodURLDescriptionHTTP
GET/api/v1/system/healthTCP 200 ms probe of 8 components (cassandra / redis / mqtt / node_red / opc_ua / edge_core / tse / grafana) + overall status200 (all UP) / 503 (DEGRADED)
GET/api/v1/system/readymonitor + V5 api_client readiness200 / 503
GET/api/v1/system/versionMetadata.VERSION + BUILD_DATE + /etc/kopens/version.env image_tag + /.dockerenv container_mode200

/health example:

{
"data": {
"status": "UP",
"uptime_ms": 1720008,
"components": {
"cassandra": "UP", "redis": "UP", "mqtt": "UP",
"node_red": "UP", "opc_ua": "UP"
}
}
}

/version example:

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

The OTA upgrade.sh decides auto-rollback based on a /api/health (300 s) probe. The Docker HEALTHCHECK also uses /health.

1.4 OPC-UA viewer (OPCUAViewerAPI)

Endpoints / node tree / time series of the built-in OPC-UA server — used by the /ui/opcua screen.

MethodURLDescription
GET/ui/opcua/info[?reveal=true]Endpoint URL (TCP/TLS) / Application name / credentials of the built-in OPC-UA server (reveal=true + plaintext password when the session is authenticated)
GET/ui/opcua/treeSite → OPC → Tag tree (including NodeId). EDGE_* system OPCs are always connection_status=CONNECTED
GET/ui/opcua/history?tagId=...&minutes=N&limit=MTime series query against Cassandra tm_tag_point. Returns an ASC-sorted points array after input validation

1.5 Upgrade / system (ConfigAPI)

/config/* is separate from the v1 REST API (it is for the administrator screens), but it is documented here as well.

MethodURLDescription
GET/config/upgrade/checkFetches VERSION.JSON from product.kopens.io server-to-server and returns {result, latest_version, latest_build_date}. Returns result=ERROR on failure
POST/config/upgradeRuns bin/upgrade.sh (long-running task)
POST/config/restartRuns bin/restart.sh (Tomcat restart)
POST/config/rebootRuns bin/reboot.sh (OS reboot)
POST/config/temp-cleanRuns bin/clean.sh (cleans up logs/temporary files)
POST/config/backupRuns bin/backup.sh
POST/config/firmwareRuns bin/firmware.sh (dnf update -y)
GET/config/loadReturns app.properties as text
POST/config/saveSaves app.properties

2. Request / response examples

2.1 GET /api/v1/opc — OPC list

curl -s http://<edge-host>/api/v1/opc | jq
{
"result": "OK",
"data": {
"data": [
{
"opc_id": "OPC_UA_Kepware",
"opc_type": "OPCUA",
"opc_name": "OPC_UA_Kepware",
"opc_agent_ip": "192.168.0.40",
"opc_agent_port": "49320",
"auto_collect": "true",
"timecycle": "1000",
"options": {"username": "kopens", "password": "***", "discovery": "false"},
"tag_count": 11,
"point_count": 5819,
"connection_status": "CONNECTED",
"scan_status": "START"
}
]
}
}

⚠ Takes the form data.data[] (double wrap) — see the envelope section above.

2.2 POST /api/v1/opc — Register an OPC together with its tags

curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_NEW",
"opc_type": "OPCUA",
"opc_name": "신규 연결",
"opc_agent_ip": "10.0.0.10",
"opc_agent_port": "49320",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"tag_list": [
{"tag_id": "TAG_001", "tag_name": "Sine",
"plc_address": "ns=2;s=Sine1", "data_type": "Float"}
]
}'

2.3 POST /api/v1/opc/{opcId}/tag — Register a single tag

{
"tag_id": "TAG_NEW",
"tag_name": "신규 태그",
"plc_address": "ns=2;s=NewTag",
"data_type": "Float",
"description": "..."
}

Response:

{ "result": "OK", "data": { "result": "SUCCESS", "tag_id": "TAG_NEW" } }

site_id is filled in automatically from the OPC.

2.4 PUT /api/v1/tag/{tagId} — Partial tag update

The request body carries only the fields to change. The server merges with the existing row and then upserts.

{ "description": "변경된 설명" }

2.5 GET /api/v1/tag/{tagId}/value — Query the last value

Default (cache):

curl -s http://<edge-host>/api/v1/tag/TAG_UA_0004/value | jq
{
"result": "OK",
"data": {
"tag_id": "TAG_UA_0004",
"value": "28.9688",
"value_time": "2026-05-06 20:59:39.000",
"value_read_status": "SUCCESS",
"value_read_error_message": ""
}
}

fresh=true (direct PLC read):

curl -s 'http://<edge-host>/api/v1/tag/TAG_UA_0004/value?fresh=true' | jq

On PLC read failure, the outer result=ERROR + message:

{ "result": "ERROR", "message": "fresh read failed: ..." }

⚠ Querying a nonexistent tag still responds with envelope result=OK and returns a placeholder payload whose value is filled with "-" (current behavior).

2.6 POST /api/v1/tag/{tagId}/value — Write a value

curl -X POST http://<edge-host>/api/v1/tag/TAG_HTTP_001/value \
-H "Content-Type: application/json" \
-d '{ "value": "42" }'

Success:

{
"result": "OK",
"data": {
"result": "SUCCESS",
"opc_id": "OPC_LS_TEST",
"plc_address": "D1000",
"value": "42",
"actual_read": "42"
}
}

Failure (driver returns false / OPC DISCONNECTED / scheduler not running) — outer envelope result=ERROR:

{ "result": "ERROR", "message": "쓰기 실패 (opc_type=EIP) — EIP 펌웨어/패치에 따라 ..." }

Supported protocols: HTTP / OPC-UA / Modbus / MELSEC / S7 / LS XGT / EIP — all supported. EIP may have limitations depending on PLC firmware/patch level.

2.7 POST /api/v1/opc/{opcId}/start — Start collection

curl -X POST http://<edge-host>/api/v1/opc/OPC_NEW/start

Automatic startup of the built-in OPCUA/MODBUS simulators is not supported. For test/demo data, use an external plantpulse-simulator or test tags.

2.8 GET /api/v1/edge — Edge identity / operating information

Fields populated directly by EdgeAPI.edgeInfo():

{
"result": "OK",
"data": {
"id": "EDGE_00303",
"site_id": "SITE_00001",
"site_name": "S1_LOTTE_CS_DJ_SITE",
"hostname": "EDGE-303",
"product_name": "PlantPulse Edge",
"version": "2026",
"build_date": "2026-05-08",
"os_name": "Linux",
"os_version": "6.14.5-100.fc40.x86_64",
"os_arch": "amd64",
"started": true,
"started_date": 1778066700000,
"uptime_ms": 124500,
"always_on": false,
"ttl": 30,
"sended_count": 8063
}
}

2.9 GET /api/v1/monitoring — System metrics

Serializes all fields of MonitorBean as-is. Refreshed every second.

curl -s http://<edge-host>/api/v1/monitoring | jq '.data | keys'

Representative fields: cpu_used_percent, memory_used_percent, disk_used_percent, temperature, ping, api, opc_count, tag_count, mps, mps_history, queue_size, plc_value_read_success_count, plc_value_read_error_count, plc_value_write_success_count, plc_value_write_error_count, plc_con_connected_count, plc_con_disconnected_count, plc_scan_start_count, plc_scan_not_collect_count, plc_scan_stop_count, sended_point_count, sended_point_bytes, system_total_db_size, system_error_count, docker_on, docker_container_up_count, docker_container_total_count.

2.10 GET /api/v1/transfer — Outbound transfer health

curl -s http://<edge-host>/api/v1/transfer | jq
{
"result": "OK",
"data": {
"transfers": [
{ "type": "API", "enabled": true, "connected": true, "sent_count": 8063 },
{ "type": "MQTT", "enabled": true, "connected": true, "sent_count": 12345 },
{ "type": "Sparkplug", "enabled": true, "connected": true,
"group_id": "Plant1", "edge_node_id": "EDGE_00303",
"bdSeq": 7, "seq": 211 }
]
}
}

Returns the getStatus() result of each transfer as an array, as-is. If an exception occurs during the call, it falls back to {type, status_error}.

2.11 GET /ui/opcua/info — Built-in OPC-UA server information

curl -s http://<edge-host>/ui/opcua/info | jq
{
"result": "OK",
"data": {
"application_name": "PlantPulse Edge OPC-UA Server",
"product_uri": "urn:plantpulse:opcua:server",
"domain": "127.0.0.1",
"tcp_endpoint": "opc.tcp://127.0.0.1:12000",
"tls_endpoint": "opc.tcp://127.0.0.1:12443",
"namespace_index": 2,
"auth_username": "edge",
"auth_password_set": true,
"auth_anonymous": false
}
}

With ?reveal=true and an authenticated session, the plaintext auth_password is added.

2.12 GET /ui/opcua/tree — Node tree

A three-level Site → OPC → Tag tree. Each tag includes its NodeId, latest value, data type, and description. EDGE system OPCs are forced to connection_status=CONNECTED.

NodeId convention: ns=2;s=<SITE>.<OPC>.<TAG>.

2.13 GET /ui/opcua/history — Time series

ParameterDefaultLimit
tagIdRequiredMax 200 characters
minutes101 – 1440 (24h)
limit6001 – 5000
curl -s 'http://<edge-host>/ui/opcua/history?tagId=TAG_S7_1000&minutes=10&limit=100' | jq
{
"result": "OK",
"data": {
"tag_id": "TAG_S7_1000",
"minutes": 10,
"count": 100,
"points": [
{ "ts": 1778176880010, "value": "211", "type": "integer", "quality": 192 }
]
}
}

If tagId is missing, blank, or exceeds 200 characters, the outer result=ERROR (message) is returned. Even if the Cassandra timestamp arrives as a wrapper object, it is converted to epoch ms via extractTimestampMs.


3. Error response patterns

CaseResponse form
Outer envelope ERROR (write failure / fresh read failure / opcua/history input validation failure / upgrade/check download failure){result:"ERROR", message:"..."} (HTTP 200)
Inner business ERROR (tag/opc not found during CRUD / DB integrity / scheduler not running){result:"OK", data:{result:"ERROR", msg:"..."}} (HTTP 200)
Spring mismapping (missing required parameter, wrong method, etc.)HTTP 500 + stack trace — recommended: handle @RequestParam(required=false) at the controller level and convert to an envelope
Authentication — edge.rest.api.auth is the installation default true, and ApiAuthFilter requires X-API-Key / Bearer (see the authentication section above). Only the health endpoints are exempt

4. Behavior notes

  • Single tag query: R03 of APP_TAG.xml (WHERE TAG_ID = :tag_id ALLOW FILTERING).
  • After registering an OPC with tags / registering a single tag, call ConnectService.restartComponents() → reloads OPCAndTagsCache and restarts the OPC-UA server.
  • Writing a value (tagWriteByTagId): HTTP uses HTTPDriver.bind(), everything else uses driver.write(ProtocolAddress). On failure (ok=false) helpful hints (EIP firmware compatibility, etc.) are included. The result of the read performed immediately after the write is also returned as actual_read (except for HTTP).
  • Transfer health (/api/v1/transfer): collects getStatus() from every outbound component registered in TransferRegistry. The Sparkplug B card (/ui/main) calls it every 5 seconds.
  • The OPC-UA viewer (/ui/opcua/*) polls on the internal network (2 s) — it uses only OPCAndTagsCache.getInstance() + LastValueMap.getInstance(), so the load is low.
  • Legacy deprecated: /api/http (APIController) — use /api/v1/tag/{tagId}/value instead.

5. Code locations

  • Controller: src/plantpulse/app/edge/api/v1/OpcAPI.java · api/v1/TagAPI.java
  • OPC-UA viewer: 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())