Device MQTT Protocol
The recommended device-ingestion transport: standard MQTT 3.1.1/5 over TLS, dual credential support, and per-message HMAC signing.
Connection & authentication
- TLS is required - the broker only accepts
mqtts://on the device port. - The MQTT
usernameMUST be the device short id shown in the credentials panel, andclientidMUST equal that same username - both are enforced by the broker. - Two password options, both work with the same username: a dedicated MQTT password (
dvp_prefix), or your existing Device API Key (dvk_prefix) for firmware that cannot be reconfigured. Whichever one you connect with is also your HMAC signing key - see below. - Set
clean_start=true(a persistent session offers no benefit to a device, and dropping it avoids an offline command queue building up while the device is disconnected). Because the session is clean, re-subscribe to yourcmdtopic on every connect - doing it from your on-connect callback covers reconnects for free. - QoS 1 on every topic. QoS 2 is not supported. Retained messages are disabled platform-wide.
- Keepalive: propose
30seconds in CONNECT - that is what every IoT Manager sample uses. It is the longest your client may stay silent before the broker drops the connection; if you publish nothing in that window, send a PINGREQ. Shorter notices a dead TCP connection sooner at the cost of more radio wake-ups; much longer lets a device that fell off the network look connected for minutes. - Keepalive is not presence: being shown offline in the dashboard is a separate, telemetry-based timeout (below), so a device that holds the connection open but stops publishing still goes offline.
mqtts://mqtt.luminatti.online:8883
username: a1b2c3d4e5f6a1b2
password: dvp_Kx9mQ2... (or your dvk_ device api key)
clientid: a1b2c3d4e5f6a1b2 (same as username)
Topic namespace
Prefix iot/v1. Every topic is rooted at your device's short topic_id - the broker rejects publish/subscribe attempts outside these exact paths.
| Topic | Direction | QoS |
|---|---|---|
| iot/v1/<topic_id>/telemetry | device → backend (single reading or a batch) | 1 |
| iot/v1/<topic_id>/cmd | backend → device (subscribe) | 1 |
| iot/v1/<topic_id>/cmd/ack | device → backend | 1 |
<topic_id> comes from your credentials panel - a short (12-character), opaque id, independent of your project/device ids. Unlike your username, it can be rotated on its own from the credentials panel without affecting your password or signing key. A device may only publish to its own telemetry/ack topics and subscribe to its own cmd topic - any other topic is denied at CONNECT-time ACL.
There is no presence/status topic - a device can never reliably announce going offline anyway. Instead, a device is marked offline once it has sent nothing for 120 seconds (configurable per device from the dashboard, minimum 30).
HMAC payload signing (LM1)
Every telemetry and command-ack payload is signed - there is no unsigned mode. The signing key is whichever password you connected with: your dvp_ MQTT password, or your dvk_ API key if you used that instead. Nothing extra to store - the same secret that authenticates your connection also signs your frames.
Frame shape: LM1.<mac>.<ts>.<body>. mac is the lowercase-hex HMAC-SHA256 over the exact bytes topic + "\n" + ts + "\n" + body (not a re-serialized/re-ordered JSON structure) - build it with sprintf + HMAC, it does not require a JSON library on the firmware side.
ts is a Unix timestamp in seconds. A frame is rejected if ts is more than 3600s old or more than 300s in the future (bring your device clock in sync with NTP).
The sequence field inside the telemetry payload must strictly increase per device - a replayed or non-increasing sequence is silently dropped, even if the signature and timestamp are both valid.
topic + "\n" + ts + "\n" + body
topic: iot/v1/demo00000042/telemetry
key: dvp_0000000000000000000000000000
ts: 1735689600
body: {"sequence":1,"event_id":"vector-1","values":{"x":{"value":1}}}
frame: LM1.67a3a4b8cf766ec815d5ce261c51f85664a3e44f956654a5616c654050b94377.1735689600.{"sequence":1,"event_id":"vector-1","values":{"x":{"value":1}}}
Telemetry - single measurement
There is no per-message ACK on this transport - the QoS 1 PUBACK is the only delivery confirmation, and it means broker receipt, not backend acceptance; use cmd/ack round trips or the dashboard to confirm ingestion.
values maps a signal key to a value object. A value may be a number, an integer, a boolean or a string - nothing else. unit is a free-form string (default empty) and quality is one of valid / stale / uncertain / error (default valid).
timestamp is the device-observed time (UTC, ISO-8601); the server records its own received-at separately, so buffered telemetry still lands at the right time. event_id is an optional idempotency key. sequence is required in practice - see the signing section above.
iot/v1//telemetry
{
"timestamp": "2026-08-22T17:00:00Z",
"sequence": 42,
"event_id": "optional-idempotency-key",
"values": {
"temperature": {"value": 24.7, "unit": "C", "quality": "valid"},
"humidity": {"value": 61.2, "unit": "%", "quality": "valid"}
},
"metadata": {}
}
Telemetry - batch
Published on the same telemetry topic as a single measurement - the backend tells them apart by the top-level messages key, not by topic. Each item is validated exactly like a single telemetry message, including its own sequence check.
sequence must strictly increase WITHIN the batch as well as across batches: the backend walks the list in order and drops the ENTIRE batch at the first non-increasing value.
iot/v1//telemetry
{
"messages": [
{ "timestamp": "...", "sequence": 41, "event_id": "...", "values": {"...": {"value": 1}} },
{ "timestamp": "...", "sequence": 42, "event_id": "...", "values": {"...": {"value": 2}} }
]
}
Commands
Subscribe to your cmd topic at connect time. Every command the backend publishes there is itself LM1-signed with your connection password, exactly like your own outbound frames - verify it the same way before acting on it.
command is the target datastream's key (e.g. "relay1"), never a verb - and kind is derived server-side from that datastream, always one of exactly action / boolean / number / string / enum. A kind of "action" never carries a value field at all; every other kind requires one of the matching type.
Reply on cmd/ack with the same command_id, signed. status must be exactly the string "ok" for success; anything else is treated as a device error and error is read as the failure reason. A command that gets no ack within the configured timeout is marked timed out server-side.
{"type": "COMMAND", "command_id": "...", "command": "gate1", "kind": "action"}
{"type": "COMMAND", "command_id": "...", "command": "relay1", "kind": "boolean", "value": true}
{"command_id": "...", "status": "ok", "value": true}
{"command_id": "...", "status": "error", "error": "actuator_fault"}
Presence: offline timeout
There is nothing to publish here - a device does not announce its own presence. The dashboard marks a device offline once it has received no telemetry for that device's configured offline timeout (default 120s, adjustable per device, minimum 30s). There is no faster path than that: no status topic, no Last Will, nothing for the device to set up.
Message limits
| Limit | Applies to |
|---|---|
| 50 | signal keys in one values map |
| 100 | messages in one batch ({"messages": [...]}) |
| 65536 | bytes for the whole published frame - the LM1 signature prefix included, not just the body |
The 64 KB cap is checked before the signature is even verified, so an oversized frame is dropped with no diagnosis whatsoever. Split a backlog by BYTES as well as by message count: 100 small readings fit easily, 100 wide multi-signal readings may not. Separately, your plan caps how many DISTINCT signal keys one device may ever use (20 on every plan today) - see the <a href="/docs/guides#rate-limits">Guides</a>.
Rejection behavior
- There is no ERROR frame on this transport - a rejected message (bad signature, stale timestamp, replayed sequence, oversized payload, malformed JSON) is simply dropped after the broker's own PUBACK, which only confirms broker-side receipt, not backend acceptance.
- Use a
cmd/ackround trip, or the dashboard's live telemetry view, to positively confirm your device is being accepted - do not assume PUBACK means ingested. - A revoked or regenerated credential gets kicked from the broker immediately - reconnect with the new credential from the panel.
Rate limits
Same per-device, per-tier limits as every other ingestion transport, paced on your signed payload timestamp rather than arrival time - a burst of backlog delivered after a reconnect is accepted as long as the payload timestamps are properly spaced. See the full table on the <a href="/docs/guides#rate-limits">Guides</a> page.