Skip to main content

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/resp as one or more chunked messages using data_b64 (never a separate top-level data object in v1)
  • Encoding chosen from want.accept_encoding:
    • gzip: chunks carry base64-encoded gzip bytes; after reassembly, gunzip → UTF-8 JSON
    • identity (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 successful config/resp transfer; 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"
}
  • If the device already has version and sha256 applied (matches config/meta), it may skip publishing config/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 that rid, even when have matches the current version (see config/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 correlate config/resp and config/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 use have to suppress the response: if the device sends config/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 for identity path; inflated JSON for gzip path). If the config is larger, the cloud responds with ok=false (e.g. CONFIG_TOO_LARGE).
    • want.accept_encoding (array of strings, optional): preference order. Supported tokens:
      • gzip — device accepts gzip-compressed bytes (see config/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):

  1. gzip path — when the device’s want.accept_encoding includes gzip (server preference).
  2. identity path — when the device does not offer gzip (or the server chooses uncompressed transfer).

Plus:

  1. Errorok=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
  • content_type (string, required): application/json
  • chunk_index (number, required): 0-based
  • total_chunks (number, required): total number of messages for this rid
  • chunk_bytes (number, required): server’s chosen chunk size for this transfer (may differ from want.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)

  1. For each message in order chunk_index = 0 .. total_chunks-1, base64-decode data_b64 to raw bytes.
  2. Concatenate chunk bytes in order → one gzip stream.
  3. Gunzip → UTF-8 JSON bytes (the effective config document).
  4. Validate sha256 over those JSON bytes (SHA256).

encoding example:

"encoding": "gzip+base64"

2) Identity path (identity+base64 chunks)

  1. For each message in order, base64-decode data_b64.
  2. Concatenate → raw JSON bytes (no gzip).
  3. Validate sha256 over 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 send 1970-01-01T00:00:00Z.
  • error (object or null, required): null when applied=true; otherwise an error object with code and message.

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.