API

thesada-app serves a versioned JSON REST API at /api/v1. The web dashboard and the client apps use the same contract. All request and response bodies are JSON.

Authentication

Most endpoints require authentication. Two credentials are accepted:

  • Bearer token - Authorization: Bearer <token>. Obtain one from POST /auth/login.
  • Session cookie - set by the same login; used by web-origin clients.

Tokens and session tokens are stored only as SHA-256 hashes server-side.

Status Meaning
400 malformed request - bad id, invalid JSON body, or invalid field value
401 no valid credential presented
403 authenticated but missing the required role (e.g. a non-super-admin calling pair)
404 resource not found, or outside your tenant

Errors

Every error response is a JSON object with a single error string:

{ "error": "device not found" }

Success bodies vary by endpoint (an object, an array, or { "status": "ok" }).

List limits

List endpoints accept ?limit= - default 100, maximum 500. Out-of-range or unparseable values fall back to the default.

Endpoints

Method Path Auth OK Purpose
GET /healthz none 200 liveness probe
POST /auth/login none 200 password login - returns a bearer token + user
POST /auth/logout optional 200 revoke the presented token and/or cookie
POST /auth/signup none 200 join the waitlist
GET /devices yes 200 list the tenant’s devices
GET /devices/{id} yes 200 one device
POST /devices/{id}/pair super-admin 200 issue a device client certificate
GET /devices/{id}/telemetry yes 200 telemetry readings
GET /devices/{id}/alerts yes 200 a device’s alerts
GET /alerts yes 200 the tenant’s alerts
GET /alert-subscriptions yes 200 the caller’s alert subscriptions
POST /alert-subscriptions yes 201 create a subscription
DELETE /alert-subscriptions/{id} yes 204 delete a subscription

POST /auth/magic-link is reserved and not yet available.

Auth

POST /auth/login - body { "email": ..., "password": ... }. On success returns the bearer token, its expiry, and the redacted user, and also sets the session cookie. Bad credentials return 401.

{
  "token": "<bearer token>",
  "expires_at": "2026-06-22T18:00:00Z",
  "user": {
    "id": "<uuid>",
    "email": "you@example.com",
    "display_name": "You",
    "tenant_id": "default",
    "is_admin": false,
    "is_super_admin": false
  }
}

POST /auth/logout - revokes whichever credential you present (bearer and/or cookie) and clears the cookie. Idempotent; returns { "status": "ok" }.

POST /auth/signup - body { "email": ..., "note": "<optional>" }. Always returns { "status": "ok" } - it never reveals whether an email is already known.

Devices

GET /devices returns an array of the caller tenant’s devices. GET /devices/{id} returns one (404 if it is not in your tenant). Device shape:

{
  "id": "<uuid>",
  "device_id": "<factory id>",
  "display_name": "Boiler node",
  "hardware_type": "esp32-owb",
  "firmware_version": "1.5.0",
  "paired_at": "...",
  "last_seen_at": "...",
  "created_at": "...",
  "last_uptime_seconds": 86400,
  "last_uptime_at": "..."
}

POST /devices/{id}/pair (super-admin) issues and stores a new client certificate for the device and returns it. The private key is returned once and is never stored server-side.

{
  "cn": "thesada-<tenant>-<device>",
  "serial_hex": "...",
  "not_before": "...",
  "not_after": "...",
  "cert_pem": "-----BEGIN CERTIFICATE----- ...",
  "private_key_pem": "-----BEGIN PRIVATE KEY----- ...",
  "ca_pem": "-----BEGIN CERTIFICATE----- ..."
}

GET /devices/{id}/telemetry - with no parameters, the latest reading per metric. With ?metric=<name>, the recent readings of that single metric (newest first), bounded by ?limit=. Reading shape:

{ "metric": "temp.boiler", "received_at": "...", "value_num": 72.5, "value_text": null }

GET /devices/{id}/alerts - a device’s alerts, newest first. Optional ?severity=info|warn|crit filter and ?limit=. Alert shape:

{ "id": 1234, "received_at": "...", "severity": "warn", "code": "battery_low",
  "message": "Battery low for 60 s", "delivered_email": true, "delivered_telegram": false }

Alerts and subscriptions

GET /alerts - the tenant’s recent alerts (newest first), bounded by ?limit=.

GET /alert-subscriptions - the calling user’s subscriptions.

POST /alert-subscriptions - body:

{ "channel": "email", "min_severity": "warn", "device_pk": null }

channel is email or telegram. min_severity is info, warn, or crit (default warn). device_pk is optional - omit it or send null to cover all of the user’s devices; a supplied id must belong to your tenant. Returns 201 { "status": "created" }.

DELETE /alert-subscriptions/{id} - removes one of your subscriptions. Idempotent; returns 204.


Thesada - AGPL-3.0-only (app) / GPL-3.0-only (firmware) / CC BY-NC-SA 4.0 (docs) - License

This site uses Just the Docs, a documentation theme for Jekyll.