Device HTTP API

The device-facing contract: one bearer token, three endpoints. Works from firmware, scripts, gateways — anything that speaks HTTPS.

Authentication

Every request carries the device token issued at provisioning: Authorization: Bearer <device_token>. Tokens are stored hashed and shown once; rotate from the device page (POST /v1/devices/{id}/rotate-token in the app API) if lost. A rotated token invalidates the old one immediately.

Send telemetry

POST https://api.latticeiot.com/v1/ingest/{device_id}/telemetry
Authorization: Bearer <device_token>
Content-Type: application/json

{"temp_c": 23.4, "humidity": 61, "door": "open", "lamp": true}

→ 202 Accepted
  • Body is a flat JSON object: metric name → value. Nested objects are dropped per-metric.
  • Numbers land as chartable values; strings and booleans are stored as text values.
  • A 202 means accepted for processing — data is visible in the app within seconds.

Receive commands (long-poll)

GET https://api.latticeiot.com/v1/ingest/{device_id}/commands?wait_s=20
Authorization: Bearer <device_token>

→ 200 OK
{"commands": [
  {"id": "4217", "name": "set_lamp", "params": {"on": true},
   "issued_at": "2026-07-10T18:04:00Z"}
]}

wait_s (max 20) holds the request open until a command arrives or the window elapses, so a simple loop gives near-realtime delivery without keeping sockets open. Undelivered commands queue and are returned on the next poll.

Acknowledge a command

POST https://api.latticeiot.com/v1/ingest/{device_id}/commands/{command_id}/ack
Authorization: Bearer <device_token>

→ 200 OK

Ack after your device has applied the command. Unacked commands are redelivered on subsequent polls.

Full device loop (Python)

import requests

BASE = "https://api.latticeiot.com"
DEVICE = "YOUR-DEVICE-ID"
HEADERS = {"Authorization": "Bearer YOUR-DEVICE-TOKEN"}

while True:
    requests.post(f"{BASE}/v1/ingest/{DEVICE}/telemetry",
                  json={"temp_c": 23.4}, headers=HEADERS, timeout=10)
    r = requests.get(f"{BASE}/v1/ingest/{DEVICE}/commands",
                     params={"wait_s": 20}, headers=HEADERS, timeout=30)
    for cmd in r.json().get("commands", []):
        handle(cmd["name"], cmd.get("params"))  # your code
        requests.post(f"{BASE}/v1/ingest/{DEVICE}/commands/{cmd['id']}/ack",
                      headers=HEADERS, timeout=10)

Errors & limits

StatusMeaningWhat to do
401Missing, malformed, or rotated tokenRe-check the Authorization header; rotate the token in the app if it was lost.
413Payload too large (16 KB max)Send fewer metrics per message or shorten text values.
429Rate limit exceededBack off for the number of seconds in the Retry-After header, then resume.
  • Rate limit: roughly 300 messages/min per device (5/s with short bursts allowed).
  • Up to 64 metrics per message; text values up to 256 chars.
  • Plans carry daily message caps; hitting a cap also returns 429 with Retry-After.
Treat 429 as flow control, not failure: honor Retry-After and keep the loop running. For most sensors, one reading every 5–60 seconds is plenty.

Prefer MQTT?

Self-hosted deployments expose the same data plane over MQTT — see MQTT (self-hosted). Hosted MQTT is coming; HTTPS is the recommended path on the hosted platform today.