MQTT Config Protocol v1 (device)
This page documents the device-side MQTT config protocol aligned with the IoTMER control plane (config-protocol.md) and the region config-delivery service.
It is designed for:
- A retained “latest config available” hint (
config/meta) - A pull request from the device (
config/get) - A response on
config/respas one or more chunked messages usingdata_b64(never a separate top-leveldataobject in v1) - Encoding chosen from
want.accept_encoding:gzip: chunks carry base64-encoded gzip bytes; after reassembly, gunzip → UTF-8 JSONidentity(no gzip): chunks carry base64-encoded raw JSON bytes (already uncompressed)
- A device ack/status after applying (
config/status)
All topics use the console ACL prefix:
{workspace_slug}/{device_key}/…
See also:
Topic map (what goes where)
Cloud → device (subscribe)
The device subscribes to:
{workspace_slug}/{device_key}/config/#
and expects these concrete topics:
{workspace_slug}/{device_key}/config/meta(retained){workspace_slug}/{device_key}/config/resp(not retained)
Device → cloud (publish)
The device publishes:
{workspace_slug}/{device_key}/config/get(QoS 1, not retained){workspace_slug}/{device_key}/config/status(QoS 1, not retained)
config/meta (cloud → device, retained)
Purpose: announce the latest available config version and sha256 so the device can detect changes without pulling.
Topic:
…/config/meta
Payload (JSON):
version(number, required): monotonically increasing config version.sha256(string, required): lowercase hex SHA-256 of the canonical effective config JSON bytes (same bytes as after a successfulconfig/resptransfer; see SHA256).bytes(number, optional): size in bytes of that canonical JSON (informational).updated_at(string, optional): RFC3339 timestamp when this meta was published.
Example:
{
"version": 12,
"sha256": "2b3d0b5a2e2e4b2d1e0b7b8d9a6a0aa2a07b2d8a2f3f1c2c0f9d89e2a1234567",
"bytes": 1842,
"updated_at": "2026-04-17T10:00:00Z"
}
Device behaviour (recommended)
- If the device already has
versionandsha256applied (matchesconfig/meta), it may skip publishingconfig/get(saves bandwidth and broker load). - If the device does publish
config/get(e.g. forced refresh, recovery, or policy), the cloud always responds with the full effective config for thatrid, even whenhavematches the current version (seeconfig/get).
config/get (device → cloud)
Purpose: request the effective configuration and tell the server chunking and encoding preferences.
Topic:
…/config/get
Payload (JSON):
rid(string, required): request id (UUID or ULID). Used to correlateconfig/respandconfig/status.have(object, optional): what the device believes it has already applied (logging / future optimisation). When present:have.version(number, required)have.sha256(string, required)
The cloud does not usehaveto suppress the response: if the device sendsconfig/get, it must be prepared to receive a full transfer.
want(object, required): response preferences / limits.want.chunk_bytes(number, optional): preferred maximum payload size hint; the server may clamp to its own min/max.want.max_total_bytes(number, optional): maximum decoded byte size the device accepts after reassembly (uncompressed JSON foridentitypath; inflated JSON forgzippath). If the config is larger, the cloud responds withok=false(e.g.CONFIG_TOO_LARGE).want.accept_encoding(array of strings, optional): preference order. Supported tokens:gzip— device accepts gzip-compressed bytes (seeconfig/resp)identity— device accepts uncompressed JSON bytes
Default SDK behaviour:["gzip", "identity"].
Example:
{
"rid": "c6d1d86b-9b8e-4f7a-9f63-3a2a7b1d2c3d",
"have": {
"version": 11,
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"want": {
"chunk_bytes": 4096,
"max_total_bytes": 1048576,
"accept_encoding": ["gzip", "identity"]
}
}
config/resp (cloud → device)
Topic:
…/config/resp
The response is correlated by rid. The device must ignore messages whose rid does not match the active transfer.
v1 response shapes
There are two success shapes in v1 (both use chunks and data_b64):
gzippath — when the device’swant.accept_encodingincludesgzip(server preference).identitypath — when the device does not offer gzip (or the server chooses uncompressed transfer).
Plus:
- Error —
ok=false(see below).
Successful config delivery is only chunked messages with data_b64; the response envelope has no top-level data object carrying the effective config.
Common fields (every ok=true chunk)
rid(string, required)ok(boolean, required,true)version(number, required)sha256(string, required)encoding(string, required)- Gzip path: value includes
gzip(e.g.gzip+base64) - Identity path: e.g.
identity+base64
- Gzip path: value includes
content_type(string, required):application/jsonchunk_index(number, required): 0-basedtotal_chunks(number, required): total number of messages for thisridchunk_bytes(number, required): server’s chosen chunk size for this transfer (may differ fromwant.chunk_bytes)data_b64(string, required): base64 of this chunk’s payload bytes (see encoding)
0) Error response (ok=false)
rid(string, required when known; may be empty if the request was invalid)ok(boolean, required,false)error(object, required)code(string)message(string)retryable(boolean, optional)
Example:
{
"rid": "c6d1d86b-9b8e-4f7a-9f63-3a2a7b1d2c3d",
"ok": false,
"error": {
"code": "CONFIG_TOO_LARGE",
"message": "Config exceeds device max_total_bytes",
"retryable": false
}
}
1) Gzip path (gzip+base64 chunks)
- For each message in order
chunk_index = 0 .. total_chunks-1, base64-decodedata_b64to raw bytes. - Concatenate chunk bytes in order → one gzip stream.
- Gunzip → UTF-8 JSON bytes (the effective config document).
- Validate
sha256over those JSON bytes (SHA256).
encoding example:
"encoding": "gzip+base64"
2) Identity path (identity+base64 chunks)
- For each message in order, base64-decode
data_b64. - Concatenate → raw JSON bytes (no gzip).
- Validate
sha256over those JSON bytes (SHA256).
encoding example:
"encoding": "identity+base64"
Chunk ordering
The device must reassemble chunks in chunk_index order (0 … total_chunks-1). The cloud publishes in that order. If the broker or client can deliver messages out of order, the device should buffer by chunk_index until contiguous chunks are available.
SHA256 (canonical effective JSON)
The sha256 in config/meta and every config/resp chunk is the SHA-256 (hex) of the exact UTF-8 bytes of the canonical JSON of the effective config object.
Canonical rules (must match the cloud):
- JSON object keys sorted lexicographically at every nesting level.
- No insignificant whitespace.
- Arrays keep their order.
- UTF-8 encoding.
The effective JSON object has this shape (conceptually; on the wire keys appear in canonical sorted order):
{
"capability_definitions": {},
"config": {},
"metadata": {}
}
The device must not re-serialize with a non-canonical printer and expect the hash to match. Either:
- verify the hash over the received byte string after reassembly, or
- re-encode with a canonical serializer that matches the cloud rules.
config/status (device → cloud)
Purpose: tie the transfer to rid and report apply success/failure.
Topic:
…/config/status
Payload (JSON):
rid(string, required)applied(boolean, required)version(number, required)sha256(string, required)applied_at(string, required): UTC ISO8601 (YYYY-MM-DDTHH:MM:SSZ). If RTC is unknown, the SDK may send1970-01-01T00:00:00Z.error(object or null, required):nullwhenapplied=true; otherwise an error object withcodeandmessage.
Example (success):
{
"rid": "c6d1d86b-9b8e-4f7a-9f63-3a2a7b1d2c3d",
"applied": true,
"version": 12,
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"applied_at": "2026-04-17T10:22:31Z",
"error": null
}
Example (failure):
{
"rid": "c6d1d86b-9b8e-4f7a-9f63-3a2a7b1d2c3d",
"applied": false,
"version": 12,
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"applied_at": "2026-04-17T10:22:31Z",
"error": {
"code": "JSON_SCHEMA_INVALID",
"message": "missing telemetry.period_ms"
}
}
Memory / buffer requirements (device)
For gzip path, the device needs buffer space for:
- the full concatenated gzip stream (sum of decoded chunk sizes), and
- the full inflated JSON.
For identity+base64 path, the device needs:
- the full concatenated JSON bytes (after base64 decode).
iotmer_config_ctx_t (or equivalent) should size buffers for the worst case implied by want.max_total_bytes and server limits. A reference example may use a 64 KiB split buffer; production devices should size from flash/RAM budget and max config policy.