HTTP API Reference
lplex exposes a REST + SSE API for reading frames, managing sessions, and discovering devices. CORS is enabled (Access-Control-Allow-Origin: *). Non-streaming responses support gzip compression when Accept-Encoding: gzip is sent.
Endpoints
Ephemeral streaming
GET /events
Opens an SSE stream of live CAN frames. No session, no replay.
Query parameters:
| Param | Type | Description |
|---|---|---|
pgn | uint32 | Filter by PGN (repeatable, OR'd) |
exclude_pgn | uint32 | Exclude PGN from results (repeatable, OR'd) |
manufacturer | string | Filter by manufacturer name (repeatable, OR'd) |
instance | uint8 | Filter by device instance (repeatable, OR'd) |
name | string | Filter by 64-bit CAN NAME hex (repeatable, OR'd) |
exclude_name | string | Exclude device by 64-bit CAN NAME hex (repeatable, OR'd) |
bus | string | Filter by bus name (repeatable, OR'd). Only relevant in multi-bus setups. |
decode | bool | Set to true to include decoded PGN fields inline (adds "decoded" object to each frame) |
Different filter types are AND'd together.
Response: Content-Type: text/event-stream
data: {"seq":1234,"ts":"2026-03-06T10:15:32.123Z","prio":2,"pgn":129025,"src":10,"dst":255,"bus":"can0","data":"5A1F2B3C4D5E6F70"}
data: {"seq":1235,"ts":"2026-03-06T10:15:32.145Z","prio":3,"pgn":130306,"src":22,"dst":255,"bus":"can0","data":"01A4060000030000"}
Example:
curl -N "http://inuc1.local:8089/events?pgn=129025&manufacturer=Garmin"
# Filter to a specific bus in multi-bus setups
curl -N "http://inuc1.local:8089/events?bus=can1"
GET /history
Queries journal files for historical CAN frames within a time range. Requires journaling to be enabled.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
from | RFC3339 | yes | Start time |
to | RFC3339 | no | End time (default: now) |
pgn | uint32 | no | Filter by PGN (repeatable) |
src | uint8 | no | Filter by source address (repeatable) |
bus | string | no | Filter by bus name (repeatable). Multi-bus only. |
limit | int | no | Max frames to return (default: 10000) |
interval | duration | no | Downsample interval (e.g., 1s, 5s, PT1M). Keeps one frame per (source, PGN) per time bucket. |
Response: Content-Type: application/json
[
{"seq":1234,"ts":"2026-03-06T10:15:32.123Z","prio":2,"pgn":129025,"src":10,"dst":255,"data":"5A1F2B3C4D5E6F70"},
{"seq":1235,"ts":"2026-03-06T10:15:33.145Z","prio":2,"pgn":129025,"src":10,"dst":255,"data":"5B1F2C3D4E5F7071"}
]
Example:
# GPS position for the last hour
curl "http://inuc1.local:8089/history?from=$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)&pgn=129025"
# All frames from a specific time range
curl "http://inuc1.local:8089/history?from=2026-03-06T10:00:00Z&to=2026-03-06T11:00:00Z&limit=1000"
# Downsample GPS to 1 reading per second (reduces 10Hz to 1Hz)
curl "http://inuc1.local:8089/history?from=2026-03-06T10:00:00Z&to=2026-03-06T11:00:00Z&pgn=129025&interval=1s"
GET /ws
Opens a WebSocket connection for bidirectional communication. CAN frames flow to the client (same filtering as /events), and the client can send CAN frames back (same validation as /send).
Query parameters: Same as GET /events (pgn, exclude_pgn, manufacturer, instance, name, exclude_name, bus).
Message format (JSON envelope):
// Server → Client (CAN frame)
{"type": "frame", "data": {"seq":1234,"ts":"...","prio":2,"pgn":129025,"src":10,"dst":255,"data":"5A1F..."}}
// Server → Client (device event)
{"type": "device", "data": {...}}
// Client → Server (send CAN frame)
{"type": "send", "data": {"pgn":59904,"dst":255,"prio":6,"data":"0102030405060708"}}
// Server → Client (error response)
{"type": "error", "data": "send is disabled"}
Example (JavaScript):
const ws = new WebSocket("ws://inuc1.local:8089/ws?pgn=129025");
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "frame") console.log(msg.data);
};
// Send a CAN frame
ws.send(JSON.stringify({type: "send", data: {pgn: 59904, dst: 255, prio: 6, data: "0102030405060708"}}));
Buffered sessions
PUT /clients/{clientId}
Create or reconnect a buffered session. The client ID must be 1-64 alphanumeric characters, hyphens, or underscores.
Request body:
{
"buffer_timeout": "PT5M",
"filter": {
"pgn": [129025, 130306],
"exclude_pgn": [60928],
"manufacturer": ["Garmin"],
"instance": [0],
"name": ["00A1B2C3D4E5F600"],
"exclude_name": ["00DEADBEEFCAFE00"]
}
}
Only buffer_timeout is required. filter is optional.
Response: 200 OK
{
"client_id": "myapp",
"seq": 5000,
"cursor": 4800,
"devices": [
{"src": 10, "name": "0x00A1B2C3D4E5F600", "manufacturer": "Garmin", ...}
]
}
| Field | Description |
|---|---|
seq | Current head sequence number |
cursor | Where this client will resume from (last ACK'd + 1) |
devices | Snapshot of known devices |
GET /clients/{clientId}/events
Opens an SSE stream for a buffered session. Replays buffered frames from the cursor, then transitions to live streaming.
Response: Content-Type: text/event-stream (same format as GET /events)
Returns 404 if the session does not exist or has expired.
PUT /clients/{clientId}/ack
Acknowledge receipt of frames up to the given sequence number. Advances the session cursor.
Request body:
{
"seq": 1500
}
Response: 204 No Content
Frame transmission
Both /send and /query are disabled by default. Enable them with -send-enabled or the send.enabled config option. Use send.rules to define ordered allow/deny rules with PGN ranges and CAN NAME lists — see Configuration.
POST /send
Send a CAN frame to the bus.
Request body:
{
"pgn": 129025,
"src": 10,
"dst": 255,
"prio": 2,
"data": "0102030405060708"
}
| Field | Type | Description |
|---|---|---|
pgn | uint32 | PGN number |
src | uint8 | Source address |
dst | uint8 | Destination (255 for broadcast) |
prio | uint8 | Priority (0-7, lower is higher priority) |
data | string | Hex-encoded payload |
bus | string | Target bus name (optional, multi-bus only). Routes the frame to a specific CAN interface. |
Response: 202 Accepted
POST /query
Send an ISO Request (PGN 59904) asking devices to transmit a specific PGN, then wait for the response. This is the primary way to query on-demand PGNs (e.g., address claim, product info) from specific devices.
Request body:
{
"pgn": 129025,
"dst": 255,
"timeout": "PT5S"
}
| Field | Type | Description |
|---|---|---|
pgn | uint32 | PGN to request (required) |
dst | uint8 | Destination address (default 255 for broadcast) |
timeout | string | ISO 8601 duration to wait for response (default PT2S) |
bus | string | Target bus name (optional, multi-bus only). Routes the request to a specific CAN interface. |
Response: 200 OK — the first matching frame as JSON (same format as SSE frame events).
Error responses:
400 Bad Request— missing PGN or invalid timeout503 Service Unavailable— tx queue full504 Gateway Timeout— no response within timeout
Example:
# Request position from all devices
curl -X POST http://localhost:8089/query \
-H 'Content-Type: application/json' \
-d '{"pgn": 129025}'
# Request product info from device at address 10
curl -X POST http://localhost:8089/query \
-H 'Content-Type: application/json' \
-d '{"pgn": 126996, "dst": 10, "timeout": "PT5S"}'
Device discovery
GET /devices
Returns a snapshot of all discovered NMEA 2000 devices.
Response: 200 OK
[
{
"src": 10,
"bus": "can0",
"name": "0x00A1B2C3D4E5F600",
"manufacturer": "Garmin",
"manufacturer_code": 229,
"device_class": 25,
"device_function": 130,
"device_instance": 0,
"unique_number": 123456,
"product_code": 1234,
"model_id": "GPS 19x HVS",
"software_version": "5.60",
"model_version": "1",
"model_serial": "ABC123",
"first_seen": "2026-03-06T10:00:00Z",
"last_seen": "2026-03-06T10:15:32Z",
"packet_count": 45023,
"byte_count": 360184
}
]
Last-known values
GET /values
Returns the last-seen frame for each (device, PGN) pair.
Query parameters: Same as GET /events (pgn, manufacturer, instance, name, bus).
Response: 200 OK
[
{
"name": "0x00A1B2C3D4E5F600",
"src": 10,
"bus": "can0",
"manufacturer": "Garmin",
"model_id": "GPS 19x HVS",
"values": [
{
"pgn": 129025,
"ts": "2026-03-06T10:15:32.123Z",
"data": "5A1F2B3C4D5E6F70",
"seq": 1234
}
]
}
]
GET /values/decoded
Same as /values but with decoded PGN fields added.
Response: 200 OK
[
{
"name": "0x00A1B2C3D4E5F600",
"src": 10,
"manufacturer": "Garmin",
"model_id": "GPS 19x HVS",
"values": [
{
"pgn": 129025,
"ts": "2026-03-06T10:15:32.123Z",
"data": "5A1F2B3C4D5E6F70",
"seq": 1234,
"decoded": {
"latitude": 47.6062,
"longitude": -122.3321
}
}
]
}
]
Replication status
GET /replication/status
Returns replication connection and sync state. Only available when replication is configured.
Response: 200 OK
{
"connected": true,
"instance_id": "boat-001",
"local_head_seq": 50000,
"cloud_cursor": 49950,
"holes": [
{"start": 10000, "end": 10500}
],
"live_lag": 50,
"backfill_remaining_seqs": 500,
"last_ack": "2026-03-06T10:15:30Z"
}
Health and metrics
GET /healthz
Full health check endpoint. Returns structured health status including broker stats, replication state, and component health.
Response: 200 OK (ok or degraded), 503 Service Unavailable (unhealthy)
{
"status": "ok",
"broker": {
"status": "ok",
"frames_total": 123456,
"head_seq": 123456,
"device_count": 5,
"ring_entries": 12000,
"ring_capacity": 65536
}
}
Reports degraded when the CAN bus has been silent longer than bus-silence-threshold or replication is disconnected.
GET /livez
Liveness probe. Always returns 200 OK if the process is running. Use for Kubernetes livenessProbe.
Response: 200 OK
{"status": "ok"}
GET /readyz
Readiness probe. Returns 200 OK when the service is ready to handle traffic (status "ok" or "degraded"), 503 when unhealthy. Same response body as /healthz. Use for Kubernetes readinessProbe.
GET /metrics
Prometheus metrics endpoint.
Frame JSON format
Every frame in the SSE stream and API responses uses this format:
{
"seq": 1234,
"ts": "2026-03-06T10:15:32.123Z",
"prio": 2,
"pgn": 129025,
"src": 10,
"dst": 255,
"bus": "can0",
"data": "5A1F2B3C4D5E6F70"
}
| Field | Type | Description |
|---|---|---|
seq | uint64 | Monotonically increasing sequence number (starts at 1) |
ts | string | RFC 3339 timestamp with millisecond precision |
prio | uint8 | CAN priority (0-7) |
pgn | uint32 | NMEA 2000 Parameter Group Number |
src | uint8 | Source address (0-253) |
dst | uint8 | Destination address (255 = broadcast) |
bus | string | CAN interface name (e.g. "can0"). Present in multi-bus setups. |
data | string | Hex-encoded payload bytes |
Cloud API
The cloud server (lplex-cloud) exposes a similar API namespaced by instance. See Cloud Overview for the full cloud endpoint reference.