Step 4 of 7

Connect via WebSocket

Program your device to open a WebSocket connection, authenticate, and start sending data. Copy-paste examples for Node.js, Python, and ESP32.

Not deployed yet? Check out the Deployment Options page to choose between DigitalOcean, Railway, or Fly.io — especially if your device can't do TLS.

Connection Details

WebSocket URL

wss://[DOMAIN]/ws

Production (port 443, TLS)

ws://localhost:3000/ws

Local dev only

Protocol

JSON (generic) or XML (biometric)

Port 3000 is internal only. Use wss://YOUR_DOMAIN (port 443) for production. Port 3000 only works for local development. Get your device_id and token from the Devices page after adding your device.

Physical Terminal: Server Menu Fields
For biometric terminals (TM20 / Q9) the domain and port are entered as separate fields in the on-device Server menu — not as a full URL.
Device FieldSet ToNotes
Server modeYesEnables cloud / server connection mode.
Use domainNmYesConnect by domain name instead of a raw IP.
DomainNmiotaccess.xyzDomain only — no https://, no port, no /ws path.
SerPortNo443Public TLS port. Do not use 3000 (internal only).
Heartbeat3Keep-alive interval (vendor default is fine).
Server approvalNoDevice still appears as a pending "Detected" terminal to approve in the dashboard.

Port 443 means TLS. Railway terminates TLS at its edge and only exposes the public domain on 443, so the terminal must negotiate a secure (wss://) connection. If your firmware has an SSL/TLS toggle, enable it. The internal port 3000 is not reachable from outside the container.

The server accepts WebSocket upgrades on any path, so whatever path the firmware uses internally will work — you don't need to configure /ws. Watch connections arrive live under Settings → Port watch.

Device Has No TLS? Use a Plain ws:// Port
Older terminals that can only speak plain ws:// cannot connect on 443, because the public domain forces TLS. Expose a raw, non-TLS TCP port instead.

Why 443 fails without TLS:Railway's edge terminates TLS on the public domain (iotaccess.xyz), so a plain ws:// handshake on 443 is rejected. There is no way to turn TLS off on the main domain — you need a separate raw TCP port.

1

In Railway → your service → Settings → Networking → TCP Proxy, click Add TCP Proxy and set the target port to 8080. Railway returns a public host:port like turntable.proxy.rlwy.net:23456.

2

In Railway → Variables, add WS_TCP_PORT=8080 (must match the target port above), then redeploy. The server starts a plain-WebSocket listener on that port.

3

Point the device's Server menu at the proxy endpoint Railway gave you.

Device FieldSet To (example)Notes
Use domainNmYesConnect by the proxy host name.
DomainNmturntable.proxy.rlwy.netThe proxy host from Railway — not iotaccess.xyz.
SerPortNo23456The external port Railway assigned to the proxy.

This connects as plain ws://turntable.proxy.rlwy.net:23456 — no TLS, no certificates. The TCP proxy forwards raw bytes straight to the container, and the server accepts the WebSocket upgrade on any path. Confirm it arrives live under Settings → Port watch.

Connection Flow
How your device communicates with the server
1

Connect

Open WebSocket

2

Register

Send device_id + token

3

Authenticated

Receive confirmation

4

Communicate

Send/receive messages

Example Code
Copy and adapt for your device
JavaScript (Node.js)
const WebSocket = require("ws")

// For production: use wss:// with port 443 (HTTPS/TLS)
// For local dev: use ws:// with port 3000
const WS_URL = "wss://YOUR_SERVER_DOMAIN/ws"  // or "ws://localhost:3000/ws" for local dev
const DEVICE_ID = "your-device-uuid"  // From dashboard
const TOKEN = "tok_your_device_token"  // From dashboard

let ws = null
let reconnectTimer = null

function connect() {
  console.log("Connecting to", WS_URL)
  ws = new WebSocket(WS_URL)

  ws.on("open", () => {
    console.log("Connected! Registering...")
    ws.send(JSON.stringify({
      type: "register",
      device_id: DEVICE_ID,
      token: TOKEN
    }))
  })

  ws.on("message", (data) => {
    const msg = JSON.parse(data.toString())
    console.log("Received:", msg)

    if (msg.type === "command") {
      handleCommand(msg.command, msg.payload)
    }
  })

  ws.on("close", () => {
    console.log("Disconnected. Reconnecting in 5s...")
    reconnectTimer = setTimeout(connect, 5000)
  })

  ws.on("error", (err) => {
    console.error("WebSocket error:", err.message)
  })
}

function handleCommand(command, payload) {
  console.log("Executing command:", command, payload)
  // Implement your device-specific logic here
}

function sendTelemetry(data) {
  if (ws?.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      type: "telemetry",
      payload: data
    }))
  }
}

// Start connection
connect()

// Send telemetry every 30 seconds
setInterval(() => {
  sendTelemetry({
    temperature: 22.5 + Math.random() * 2,
    humidity: 45 + Math.random() * 10
  })
}, 30000)

Required Changes

  • • Replace YOUR_SERVER_IP with actual server address
  • • Replace DEVICE_ID with UUID from dashboard
  • • Replace TOKEN with your device token

Testing

  • • Run the code on your device
  • • Check the dashboard for "Online" status
  • • Send a test command from the dashboard
Message Types Reference
DirectionTypePurpose
Device → ServerregisterAuthenticate with device_id + token
Device → ServertelemetrySend sensor data / readings
Device → ServereventReport device events
Device → ServerpingHeartbeat (server replies with pong)
Server → DevicecommandExecute action on device
Server → DeviceregisteredConfirmation of successful registration
Verify Connection
  • Device appears as 'Online' in the dashboard
  • Device name shows in the Devices list
  • Event stream shows 'device_connected' event
  • Telemetry data appears on device detail page
HTTP/HTTPS Protocol (Legacy Devices)
If your device doesn't support WebSocket or requires HTTP POST

Devices that can't establish persistent WebSocket connections can use the HTTP/HTTPS protocol instead. All communication is via POST requests with JSON payloads and JSON responses.

1. Register on Startup

json
POST /api/device-http
Content-Type: application/json

{
  "cmd": "reg",
  "sn": "YOUR_DEVICE_SN",
  "cpusn": "CPU_SERIAL",
  "devinfo": {
    "modelname": "device-model",
    "usersize": 1000,
    "firmware": "v1.0.0",
    "mac": "00:11:22:33:44:55"
  }
}

Response:
{
  "ret": "reg",
  "result": true,
  "cloudtime": "2026-06-12T10:00:00Z"
}

2. Send Attendance Logs

json
POST /api/device-http
Content-Type: application/json

{
  "cmd": "sendlog",
  "sn": "YOUR_DEVICE_SN",
  "count": 1,
  "logindex": 0,
  "record": [{
    "enrollid": 1,
    "time": "2026-06-12T09:15:30",
    "mode": 1,
    "inout": 0,
    "event": 0,
    "temp": 36.5
  }]
}

Response:
{
  "ret": "sendlog",
  "result": true,
  "cloudtime": "2026-06-12T09:16:00Z"
}

3. Heartbeat (Every 60s)

json
POST /api/device-http
Content-Type: application/json

{
  "cmd": "checklive",
  "sn": "YOUR_DEVICE_SN",
  "time": "2026-06-12T09:20:00"
}

Response:
{
  "ret": "checklive",
  "result": true,
  "cloudtime": "2026-06-12T09:20:05Z"
}

Python Example

python
import requests
import json
from datetime import datetime

URL = "https://your-host/api/device-http"
SN = "YOUR_DEVICE_SN"

# 1. Register device
def register():
    payload = {
        "cmd": "reg",
        "sn": SN,
        "cpusn": "12345",
        "devinfo": {"modelname": "device", "firmware": "v1.0"}
    }
    r = requests.post(URL, json=payload)
    print("Registered:", r.json())

# 2. Send attendance log
def send_log():
    payload = {
        "cmd": "sendlog",
        "sn": SN,
        "count": 1,
        "record": [{
            "enrollid": 123,
            "time": datetime.now().isoformat(),
            "mode": 1,
            "inout": 0
        }]
    }
    r = requests.post(URL, json=payload)
    print("Sent log:", r.json())

# 3. Send heartbeat
def heartbeat():
    payload = {
        "cmd": "checklive",
        "sn": SN,
        "time": datetime.now().isoformat()
    }
    r = requests.post(URL, json=payload)
    print("Heartbeat:", r.json()['result'])

if __name__ == "__main__":
    register()
    send_log()
    heartbeat()
Send a checklive heartbeat every 60 seconds to keep the device marked as online. Without it, the device will be marked offline after 5 minutes of inactivity.