Skip to content

Quickstart: from zero to first values

This chapter shows a minimal happy path from "I have a Coldwave backend URL" to "I see live device values in my terminal".

Runnable example

Everything in this quickstart exists as a complete, runnable Node.js project (plain ESM, one dependency, under 500 lines including comments):

Download coldwave-v5-getting-started-example.zip

Unpack it, npm install, copy .env.example to .env, fill in URL and credentials, npm start.

1. Prerequisites

You need:

  • A running Coldwave backend instance, e.g. https://<<URL>>.
  • An invite code from your administrator (or an existing account).
  • Node.js 22 or newer for the code samples (fetch and WebCrypto are used from core).

2. Create your account

An administrator invites you by email address (POST /user). You receive an invite code — either directly in your inbox or handed over out-of-band.

Redeem it once, choosing your own password. This endpoint is public, so plain curl works:

bash
curl --location 'https://<<URL>>/api/v1/user/register' \
  --header 'Content-Type: application/json' \
  --data '{
    "code": "<<INVITE_CODE>>",
    "password": "<<PASSWORD>>"
  }'
json
{ "resourceIdentifier": "crn#tenant:Ba9mN3pQ.user:Xk9mN2pQ", "emailVerified": true }
  • Passwords must be 16–64 characters — shorter ones are rejected.
  • If the invite was mailed to you, your address counts as verified (emailVerified: true) and you can log in right away.
  • Otherwise a verification code is on its way to the invite's address; confirm it with POST /user/verify-email before the first login.

3. Log in

v5 does not use plain bearer tokens: every request is signed with a key pair held by your client (DPoP), and the issued tokens are bound to that key. Three rules govern the whole session:

  • One key pair per session — for login, every request, and every refresh.
  • A fresh signed proof per request in the DPoP header.
  • A nonce handshake on the first call400 use_dpop_nonce plus a DPoP-Nonce header is normal; retry with that value in the proof.

The whole client fits in one ~60-line class — take src/dpop.js from the example above, or copy it from Authentication & Sessions. With it, logging in is one call:

js
import { DpopClient } from "./dpop.js";

const client = await DpopClient.create();

const response = await client.fetch("POST", "https://<<URL>>/api/v1/auth/login", {
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email: "<<EMAIL>>", password: "<<PASSWORD>>" }),
});
const { accessToken, refreshToken } = await response.json();

A 409 (email in several tenants) or status: "2fa_required" answer means an extra step — see Authentication & Sessions.

4. Make authenticated requests

Every authenticated call needs the access token and a fresh proof with the ath claim — the DpopClient handles both when you pass accessToken:

js
const devices = await client.fetch("GET", "https://<<URL>>/api/v1/devices?depth=2", {
    accessToken,
});

Access tokens live 10 minutes, refresh tokens 8 hours with rotation on every use — on a 401, refresh via POST /auth/token and retry once (token lifecycle).

5. List devices, services and properties

One request returns your whole fleet with all current values:

js
const response = await client.fetch(
    "GET",
    "https://<<URL>>/api/v1/devices?depth=2&expand=propertyType,modifier",
    { accessToken },
);
const devices = await response.json();
json
[
  {
    "crn": "crn#tenant:Ba9mN3pQ.device:A1B2C3D4",
    "imei": "350000000000001",
    "services": [
      {
        "serviceId": "00000000-0000-2001-8003-006d0099ab53",
        "properties": [
          { "id": "0x0800", "value": 215, "measuredAt": 1756800000000, "pending": null, "propertyType": "UINT16" }
        ]
      }
    ]
  }
]

depth folds the hierarchy into one response — 0 devices only, 1 plus services, 2 plus every current property value. At this point you can already render a device list with services and raw property values. Direct routes for single devices and services, write operations and method calls: Devices & Properties.

6. Load schemas for human-readable labels

js
const response = await client.fetch("GET", "https://<<URL>>/api/v1/schema?depth=1", { accessToken });
const schemas = await response.json();

The response maps each serviceIdentifier to names, units, enums and flags per property id. Build a local map from (serviceIdentifier, propertyId) to the schema entry and use it to show temperature instead of 0x0800, render enum dropdowns (off / silent / running), and respect readonly.

Property ids match directly — both sides use canonical hex strings ("0x0800"). Sample response, display formats and method declarations: Schemas.

7. Load metadata for nicer device lists

js
const response = await client.fetch("GET", "https://<<URL>>/api/v1/meta?depth=1", { accessToken });
const meta = await response.json();
json
[
  {
    "crn": "crn#tenant:Ba9mN3pQ.device:A1B2C3D4",
    "imei": "350000000000001",
    "meta": {
      "status": "online",
      "lastMessage": 1756800000123,
      "data": { "name": "Rooftop HVAC 1", "site": "Berlin", "floor": 4 }
    }
  }
]

status and lastMessage are maintained by the backend; data is yours — use it to show "Rooftop HVAC 1 – Berlin" instead of a CRN. Writing metadata: Metadata.

8. Open the websocket and listen for updates

Fetch a single-use ticket (valid 30 seconds), then connect — the upgrade itself carries no auth header:

js
import WebSocket from "ws";

const { ticket } = await (await client.fetch(
    "GET", "https://<<URL>>/api/v1/socket/ticket", { accessToken })).json();

const ws = new WebSocket(`wss://<<URL>>/api/v1/socket?ticket=${encodeURIComponent(ticket)}`);

ws.on("message", (raw) => {
    const event = JSON.parse(raw.toString());
    if (event.type === "HEALTHCHECK_PONG") return;
    console.log(event.type, event.resourceIdentifier);
});

There is no subscribe message — you receive every event of your tenant your account may read, e.g. property values as they arrive:

json
{
  "type": "OBJECT_UPDATED",
  "resourceIdentifier": "crn#tenant:Ba9mN3pQ.device:A1B2C3D4.service:00000000-0000-2001-8003-006d0099ab53",
  "payload": { "updates": [ { "propId": 2048, "tag": "value", "value": 220, "measuredAt": 1756800000000 } ] }
}

WARNING

propId is decimal here (2048 = 0x0800) — the one place the API departs from hex strings.

Apply incoming events on top of your REST snapshot and your UI stays live. Event catalog, heartbeat and reconnect strategy: WebSocket & Events.

Where to go next