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.
| Item | Value |
|---|---|
| Base URL | http(s)://<edge-host>/api/v1 |
| Content-Type | application/json; charset=utf-8 |
| Authentication | API 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 log | ApiAccessLogFilter — records every /api/* call on a single line (method/path/status/duration/IP/user/auth mode) |
| Main controller | plantpulse.app.edge.api.v1.* (OpcAPI, TagAPI, AppAPI, SystemAPI, MonitoringAPI) |
| Error envelope | {data, meta, errors} — identical on the filter/advice path |
| HTTPS | edge.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.
B. X-API-Key header (recommended)
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...
| Field | Meaning |
|---|---|
method | HTTP method |
path | Request URI (query excluded) |
query | Query string — the rejected ?api_key=... portion is also masked as *** |
status | HTTP response code |
duration | Processing time (ms) |
ip | RemoteAddr 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 |
user | user_id for a session user / XXXX*** (len=16) for apikey (first 4 characters + length) / - for anonymous |
auth | session (browser login) / apikey (X-API-Key/Bearer) / deprecated_query (rejected ?api_key= attempt) / none (missing key or anonymous) |
ua | User-Agent (truncated with … beyond 80 characters) |
error | error=ClassName appended when an exception occurs |
Log level branching
| Status | Level | Intent |
|---|---|---|
| 5xx or throwable | ERROR | Server error — alarm immediately |
| 401, 403 | WARN | Authentication/authorization denial — audit priority |
| Others (2xx, 3xx, general 4xx) | INFO | Routine calls |
Usage
- Who is sending an invalid key — check the 401 trail with
grep '\[API_ACCESS\].*status=401' - 5xx regression tracking —
grep '\[API_ACCESS\].*status=5'+ theerror=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.
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
| Slug | Method | URL | Description |
|---|---|---|---|
flow_edge_opc_list | GET | /api/v1/opc | OPC list (includes tag_count / connection_status / scan_status) |
flow_edge_opc_create | POST | /api/v1/opc | Register an OPC together with its tags |
flow_edge_opc_update | PUT | /api/v1/opc/{opcId} | Update an OPC together with its tags (the path opcId overrides the body opc_id) |
flow_edge_opc_delete | DELETE | /api/v1/opc/{opcId} | Delete an OPC and all of its tags at once |
flow_edge_opc_start | POST | /api/v1/opc/{opcId}/start | Start collection |
flow_edge_opc_stop | POST | /api/v1/opc/{opcId}/stop | Stop collection |
1.2 Tag
| Slug | Method | URL | Description |
|---|---|---|---|
flow_edge_tag_list | GET | /api/v1/opc/{opcId}/tag | Tag list for a specific OPC (includes last value/status) |
flow_edge_tag_create | POST | /api/v1/opc/{opcId}/tag | Register a single tag |
flow_edge_tag_update | PUT | /api/v1/tag/{tagId} | Partially update a single tag (merges with the existing row, then upserts) |
flow_edge_tag_delete | DELETE | /api/v1/tag/{tagId} | Delete a single tag |
flow_edge_tag_read | GET | /api/v1/tag/{tagId}/value[?fresh=true] | Cached value (default); with fresh=true, reads directly from the PLC |
flow_edge_tag_write | POST | /api/v1/tag/{tagId}/value | Write a value — all protocols supported (HTTP / OPC-UA / Modbus / MELSEC / S7 / LS / EIP) |
1.3 Monitoring / Edge / Transfer
| Slug | Method | URL | Description |
|---|---|---|---|
flow_edge_monitoring | GET | /api/v1/monitoring | All fields of MonitorBean: CPU/memory/network/disk/threads, etc. |
flow_edge_info | GET | /api/v1/edge | Edge identity/version/site/OS/uptime metadata |
flow_edge_transfer | GET | /api/v1/transfer | Health 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).
| Method | URL | Description | HTTP |
|---|---|---|---|
| GET | /api/v1/system/health | TCP 200 ms probe of 8 components (cassandra / redis / mqtt / node_red / opc_ua / edge_core / tse / grafana) + overall status | 200 (all UP) / 503 (DEGRADED) |
| GET | /api/v1/system/ready | monitor + V5 api_client readiness | 200 / 503 |
| GET | /api/v1/system/version | Metadata.VERSION + BUILD_DATE + /etc/kopens/version.env image_tag + /.dockerenv container_mode | 200 |
/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.
| Method | URL | Description |
|---|---|---|
| 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/tree | Site → OPC → Tag tree (including NodeId). EDGE_* system OPCs are always connection_status=CONNECTED |
| GET | /ui/opcua/history?tagId=...&minutes=N&limit=M | Time 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.
| Method | URL | Description |
|---|---|---|
| GET | /config/upgrade/check | Fetches VERSION.JSON from product.kopens.io server-to-server and returns {result, latest_version, latest_build_date}. Returns result=ERROR on failure |
| POST | /config/upgrade | Runs bin/upgrade.sh (long-running task) |
| POST | /config/restart | Runs bin/restart.sh (Tomcat restart) |
| POST | /config/reboot | Runs bin/reboot.sh (OS reboot) |
| POST | /config/temp-clean | Runs bin/clean.sh (cleans up logs/temporary files) |
| POST | /config/backup | Runs bin/backup.sh |
| POST | /config/firmware | Runs bin/firmware.sh (dnf update -y) |
| GET | /config/load | Returns app.properties as text |
| POST | /config/save | Saves 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=OKand returns a placeholder payload whosevalueis 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
| Parameter | Default | Limit |
|---|---|---|
tagId | Required | Max 200 characters |
minutes | 10 | 1 – 1440 (24h) |
limit | 600 | 1 – 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
| Case | Response 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:
R03ofAPP_TAG.xml(WHERE TAG_ID = :tag_id ALLOW FILTERING). - After registering an OPC with tags / registering a single tag, call
ConnectService.restartComponents()→ reloadsOPCAndTagsCacheand restarts the OPC-UA server. - Writing a value (
tagWriteByTagId): HTTP usesHTTPDriver.bind(), everything else usesdriver.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 asactual_read(except for HTTP). - Transfer health (
/api/v1/transfer): collectsgetStatus()from every outbound component registered inTransferRegistry. 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 onlyOPCAndTagsCache.getInstance()+LastValueMap.getInstance(), so the load is low. - Legacy deprecated:
/api/http(APIController) — use/api/v1/tag/{tagId}/valueinstead.
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())