MQTT First Steps
First steps with KiloCenter MQTT — enable the disabled-by-default broker, subscribe to events, publish downlinks.
KiloCenter's MQTT integration lets your application react to MIOTY network messages as they arrive and publish requests such as downlinks. It is the message-stream route from the service center into software you operate.
A broker forwards published messages to subscribers. Topics name those message channels. Subscribe to the service-center events your application needs and publish commands using the documented topic and payload format.
Use this path when your application works with message streams rather than direct gRPC calls. Have KiloCenter and a reachable broker running, then enable MQTT in the configuration used by your installation. The integration is disabled by default; starting a broker alone does not enable it.
What does this guide achieve?
Enable MQTT integration and validate event consumption and command publishing.
Prerequisites
MQTT is disabled by default in KiloCenter. Before using MQTT:
Ensure the Mosquitto broker is running (included in the Docker Compose stack).
Enable MQTT in your KiloCenter configuration:
mqtt:
enabled: true
host: "localhost"
port: 1883
username: "admin"
password: "KiloCenter"
topic_prefix: "mioty"
enable_command_subscriptions: falseNote: Set
enable_command_subscriptions: trueif you want KiloCenter to accept downlink commands via MQTT. Restart KC-Core after any configuration change.
Restart KC-Core for the change to take effect.
Local Broker Defaults
Broker (TCP)
localhost:1883
Broker (WebSocket)
localhost:9001
Username
admin
Password
KiloCenter
Topic prefix
mioty (configurable in config.yaml)
MQTT Wildcards
MQTT supports two wildcard characters in subscription topics:
+matches a single topic segment (e.g.,mioty/+/device/+/event/upmatches any org and device).#matches all remaining segments (e.g.,mioty/<ORG_UUID>/device/#matches all device topics for one org).
Topics are case-sensitive.
Topic Contract
All MQTT topics follow this format:
{prefix}defaults tomioty.{org_uuid}is your organization UUID.{ep_eui_hex}is the 16-character lowercase hex EUI of the endpoint (e.g.,70b3d59cd00009e6).
Event Topics (Published by KiloCenter)
Supported event_type values:
up
Uplink data received from a device
attach
Endpoint attached to a base station
detach
Endpoint detached from a base station
downlink_queued
A command/down downlink was accepted and queued
downlink_rejected
A command/down downlink was refused, with the reason
downlink_result
Result of a queued downlink, and the endpoint's acknowledgement of it
Command Topic (Consumed by KiloCenter)
Subscribe to Events
Subscribe to all events for one organization:
Subscribe to uplinks only:
Subscribe to a single device:
Publish a Downlink Command
Replace <ORG_UUID> with your organization UUID and <EP_EUI_HEX> with your endpoint's EUI in hex format.
Observe Downlink Outcomes
Every downlink command you publish is answered on event/downlink_queued or event/downlink_rejected, and a queued downlink later reports on event/downlink_result:
Message Payloads
event/up
epEuiis the endpoint; it is also the device segment of the topic.bsEui,rssi,snr,eqSnr,rxTime,rxDuration,profile,modeandsubpacketsdescribe the first reception;baseStationslists every base station that received the uplink, each with its own metrics, and a base station that reported downlink reception quality addsdlRxSnranddlRxRssi.cntis the end point's uplink packet counter for this message.datais base64-encoded payload bytes;formatis the uplink's payload format number.rxTimeandrxDurationare nanoseconds.rxTimeis larger than 2^53, so a JavaScript client that needs every nanosecond must read it with a big-integer JSON parser.subpacketsholds the per-subpacketsnr(dB),rssi(dBm),frequency(Hz) and, when the base station reports it,phase(degrees).dlOpenistruewhen the endpoint opened a downlink window after this uplink; a queued downlink can be sent in it.responseExpistruewhen the endpoint expects a response in that window.dlAckistruewhen the endpoint acknowledges the downlink it received in its previous window.duplicateistruewhen the endpoint reused a packet counter, withpacketCntReusedwhen that happened outside the duplicate window.decodeStatusissuccess,failed,skipped(no blueprint) orpending. On successdecodedPayloadholds the decoded values andblueprintTypeEuinames the blueprint's device type; on failuredecodeErrorCodenames the reason.
event/attach
event/detach
Each attachment and detachment is published once. bsEui names the base station that heard an over-the-air attach or detach; an attach or detach made in KC-Web, through the API or by an Application Center has no bsEui.
event/downlink_queued
queIdidentifies the downlink in the laterevent/downlink_result. It is always at most 2^53-1, so every JSON client reads it exactly.refechoes therefof your command and is omitted when the command had none. KiloCenter stores it with the downlink, so every laterevent/downlink_resultof the downlink carries it too.
event/downlink_rejected
code is stable and meant for your client to match on; message explains it. Refusals detected in the command itself:
code
Reason
mqtt.command.empty_payload
The MQTT message is empty
mqtt.command.message_too_large
The MQTT message exceeds 1 MB
mqtt.command.invalid_json
The message is not a JSON object
mqtt.command.invalid_field
A field has the wrong type or is out of range, such as format above 255
mqtt.command.missing_data
Neither data nor entries is present
mqtt.command.data_with_entries
Both data and entries are present
mqtt.command.empty_entries
entries is an empty array
mqtt.command.missing_packet_cnt
An entry has no packetCnt
mqtt.command.duplicate_packet_cnt
Two entries name the same packetCnt
mqtt.command.invalid_base64
A data value is not valid base64
mqtt.command.payload_too_large
A decoded payload exceeds 200 bytes
mqtt.command.org_unresolved
The organization in the topic does not exist; a command with a ref whose organization could not be looked up at all is not answered (see A ref is accepted once below)
mqtt.command.enqueue_failed
The service center could not queue the downlink
mqtt.command.ref_too_long
ref exceeds 128 bytes
mqtt.command.expired
expiresAt passed before the downlink could be queued
Refusals by the service center core carry its catalog token, for example scaci.error.endpoint_not_found for an endpoint that is not registered to you.
A message on a malformed topic (an invalid organization UUID or endpoint EUI) cannot be answered, because it has no event topic; KiloCenter only logs it.
event/downlink_result
Possible result values:
result
When it is published
sent
A base station transmitted the downlink
expired
The downlink reached its deadline before it was sent: still in the service center queue, or at a base station that then dropped it, said it does not hold it or started a new session; or the base station holding it was deleted (see Deadline below)
invalid
A base station refused the downlink
acknowledged
The endpoint confirmed it received the transmitted downlink
Only a
sentresult carriesbsEui(the base station that transmitted the downlink) andtxTime(Unix time of transmission in nanoseconds).packetCntis the endpoint packet counter of the downlink window: the window the downlink was sent in forsent, and the window the endpoint acknowledged foracknowledged.refis therefof thecommand/downthat queued the downlink, on every result, and is omitted when the command had none. A downlink queued by an Application Center or through the API has noref.
sent, expired and invalid are final: each downlink reports exactly one of them, which, like every downlink outcome, can arrive twice (see QoS Behavior). acknowledged follows a sent result when the endpoint's next uplink sets the downlink acknowledgement flag (dlAck, which also appears on event/up) for the window the downlink was sent in. It is published once per transmitted downlink: a repeated reception of that uplink, an uplink whose acknowledgement matches no transmitted downlink, and an uplink without the flag publish nothing. When the endpoint's packet counter restarted and reused a window, the downlink transmitted last in that window is the one acknowledged. A downlink the endpoint never acknowledges publishes no acknowledged, so absence after sent means the endpoint has not confirmed it.
KiloCenter stores the acknowledgement together with its record and publishes it from its delivery outbox, the way it publishes uplinks: an acknowledgement recorded while your broker is unreachable, or carried by an uplink relayed from a federated Community Edition, is published once the broker accepts it. Like an uplink, it can arrive a second time when KiloCenter restarts between publishing it and recording that it did, so treat a repeated acknowledged for the same queId as the same confirmation.
command/down (You Publish This)
The minimal command:
Every other field is optional:
A downlink that depends on the endpoint packet counter, for example because of application encryption, gives one payload per counter in entries instead of data; it is sent only in the downlink window of a listed counter:
An empty data queues a pure acknowledgement downlink:
data
base64 string
The downlink payload; empty for a pure acknowledgement
entries
array of {packetCnt, data}
Counter-dependent payloads; mutually exclusive with data
confirmed
boolean
Request a response from the endpoint (responseExp)
prio
number
Priority, higher values first (single precision, default 0)
format
integer 0-255
User data format identifier
responsePrio
boolean
Request a priority response from the endpoint
dlWindReq
boolean
Request a further downlink window from the endpoint
expOnly
boolean
Send only when the endpoint expects a response
dlRxStatQry
boolean
Ask the endpoint for its downlink reception status
ref
string
Your correlation id of at most 128 bytes, echoed in downlink_queued, downlink_rejected and every downlink_result of the downlink
expiresAt
RFC 3339 string
The latest time the downlink may be transmitted, for example 2026-10-01T12:00:00Z or with fractional seconds and an offset
Validation rules:
Exactly one of
dataandentriesis present.Every
datavalue is valid base64 and decodes to at most 200 bytes, the radio downlink payload limit.Every entry has a
packetCnt(0 to 4294967295), and no counter repeats.ref, when present, is at most 128 bytes.expiresAt, when present, is an RFC 3339 time; anything else is refused withmqtt.command.invalid_field.Raw MQTT payload maximum: 1 MB.
A refused command is reported on
event/downlink_rejected; nothing is queued.
Deadline. A downlink waits for its endpoint's downlink window until protocol.downlink_expiry.lifetime has passed, or until expiresAt when that comes first. A downlink still in the service center queue at its deadline ends expired at once. A downlink a base station already holds may have been transmitted in the meantime, so KiloCenter asks the station to drop it and reports only what it has reason to believe: sent when the station reports the transmission, and expired in one of these cases:
the downlink was still in the service center queue when its deadline passed, so no station ever held it;
the holding station confirmed that it dropped the downlink, or answered that it does not hold it (
protocol.downlink_expiry.revoke_not_held_codes);the holding station opened a new session that is not a resumed one, which discards everything the previous session held, a pending result included (BSSCI §1);
Until one of these happens nothing is published, also while the station is offline or keeps refusing the revoke with another code; a connected station is asked again once per protocol.downlink_expiry.sweep_interval, and an offline one when it reconnects. Time passing alone never ends a downlink a connected station holds. When the station that held a downlink reports sent after it was reported expired, it contradicts the report: the result stays expired, nothing is published again, and the service center logs a warning and records one dl_data_sent_after_expiry event for the endpoint, however often the station repeats it.
Deleting a base station ends every downlink it held as expired, before or after its deadline: the ones it holds queued, the ones reserved for it and the ones it is asked to drop. Its session is closed first, and a deleted station never connects again, so it can no longer report what became of them, and none is returned to the queue, where the endpoint could receive it a second time from another station. A station that is still powered may nevertheless transmit what it held after it was deleted, so the endpoint may receive a downlink reported expired. To avoid that, power the station off, or wait until no downlink is queued at it, before you delete it.
A revoke requested by an operator or an Application Center for a downlink whose deadline has passed while a station holds it ends expired, not revoked: the station's answer settles the expiry already under way.
A command whose expiresAt has already passed when KiloCenter receives it queues nothing and is refused with mqtt.command.expired.
A ref is accepted once. KiloCenter keeps every ref an organization used for an endpoint, so you can safely publish a command again when you are not sure it arrived, for example after your client restarted. A command whose ref already queued a downlink for the same endpoint queues nothing and publishes nothing: neither downlink_queued nor downlink_rejected. The ref is compared as soon as the organization is resolved, before the payload and the endpoint are checked, so this holds while the first downlink is waiting and after it ended, also when the repeat's expiresAt has passed by then, or the endpoint was deleted or lost its downlink capability since: a repeat is never refused for something the first command passed. A field of the wrong type in a repeat does not get it refused either, as long as the ref itself is readable. If the service center cannot look the ref up, or cannot look up the organization of a command with a ref, it does not answer the command at all rather than risk refusing one it accepted; publish it again later. The first command's downlink_queued and its downlink_result are the outcome. Use a new ref for every new downlink; commands without a ref are never compared.
Copy-Paste Cookbook
Set shell variables once to simplify repeated commands:
Then use them in any command:
End-to-End Flows
Watch Device Traffic
Start a subscriber for your org:
When a device sends an uplink, you receive a message on
.../event/upwith radio metrics and payload.
Send a Downlink via MQTT
Publish a command:
KiloCenter resolves the org and validates the command. You receive
.../event/downlink_queuedwith thequeId, or.../event/downlink_rejectedwith the reason.When a base station sends the downlink, or it expires, you receive
.../event/downlink_resultwith the samequeIdand yourref.When the endpoint confirms it received a sent downlink, you receive a second
.../event/downlink_resultwith"result": "acknowledged".
Observe Attach/Detach Lifecycle
Subscribe to a single device:
When the endpoint attaches or detaches, you receive
.../event/attachor.../event/detach.
QoS Behavior
event/up
1 (at least once)
event/downlink_queued
1 (at least once)
event/downlink_rejected
1 (at least once)
event/downlink_result
1 (at least once)
event/attach
0 (at most once)
event/detach
0 (at most once)
command/down subscription
1 (at least once)
The downlink outcomes are published at least once because a platform settles its commands on them: none is lost while the broker session holds, but any of them can arrive twice. Treat a repeated downlink_queued or downlink_result for the same queId and result, or a repeated downlink_rejected for the same ref, as the same outcome. Uplinks can arrive twice for the same reason, so make your client idempotent for every QoS 1 topic.
Tenant Isolation
KiloCenter enforces data isolation through two layers:
Application-level isolation -- KiloCenter resolves endpoint ownership to the correct tenant and organization before publishing. MQTT topics are constructed from the owner's org UUID. If the owner org cannot be resolved, the publish is skipped entirely rather than risking data leakage.
Broker-level isolation -- You should enforce per-organization topic ACL rules at the MQTT broker. Without ACL rules, a wildcard subscriber could see events from multiple organizations.
Recommended ACL Pattern
Create one broker user per organization or integration and restrict topic access.
Example (Mosquitto syntax):
Do not grant broad wildcard access like mioty/+/device/+/event/# to tenant-specific clients.
Troubleshooting
No Events Received
Verify your subscribe topic matches your org UUID exactly.
Confirm ACL allows read access to your org's event topics.
Check that the device actually produced traffic or a state transition.
Verify MQTT broker connectivity and credentials.
Downlink Command Has No Effect
Confirm
mqtt.enable_command_subscriptionsis set totruein your configuration.Subscribe to
mioty/<ORG_UUID>/device/<EP_EUI_HEX>/event/downlink_rejected; a refused command is reported there with its reason.Verify the topic is exactly
mioty/<ORG_UUID>/device/<EP_EUI_HEX>/command/down.Check that ACL allows write access to your org's command topic.
Quick Broker Sanity Check
Subscribe to all topics to verify the broker is receiving messages:
Last updated