Skip to main content

MQTT

An MQTT endpoint is a small MQTT broker over WebSocket. Clients connect with the mqtt WebSocket subprotocol at:

wss://app.hooklistener.com/mqtt/<slug>

It speaks MQTT 3.1, 3.1.1 and 5. Every PUBLISH is recorded, with its topic as the event name, and by default it is also delivered to the endpoint's other subscribers. See the overview for the console, rules, scenarios, limits and the REST API.

Connect​

These are the snippets from the endpoint's Connect tab, for an endpoint with an access token. Without a token, leave out the token parts.

Browser / Node (mqtt.js)

// npm i mqtt
import mqtt from "mqtt";

// Broker mode: matching subscribers get every publish, this client included.
const client = mqtt.connect("wss://app.hooklistener.com/mqtt/<slug>?token=<token>", {
clientId: "device-1",
keepalive: 60,
protocolVersion: 4, // 5 for MQTT 5, 3 for MQTT 3.1
// Or keep the token out of the URL and send it as the CONNECT password:
// username: "hooklistener", password: "<token>",
});
client.on("connect", () => {
console.log("connected");
client.subscribe("sensors/#", { qos: 1 });
client.publish("sensors/temp", "21.5", { qos: 1 });
});
client.on("message", (topic, payload) => console.log(topic, payload.toString()));

Python (paho-mqtt)

# pip install paho-mqtt
# Broker mode: matching subscribers get every publish, this client included.
import paho.mqtt.client as mqtt

# protocol=mqtt.MQTTv5 for MQTT 5, mqtt.MQTTv31 for 3.1
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="device-1", transport="websockets")
client.ws_set_options(path="/mqtt/<slug>")
# The access token as the CONNECT password (or append ?token=... to the path)
client.username_pw_set("hooklistener", "<token>")
client.tls_set()

def on_connect(client, userdata, flags, reason_code, properties):
print("connected", reason_code)
client.subscribe("sensors/#", qos=1)
client.publish("sensors/temp", "21.5", qos=1)

client.on_connect = on_connect
client.on_message = lambda client, userdata, message: print(message.topic, message.payload)
client.connect("app.hooklistener.com", 443, keepalive=60)
client.loop_forever()

MQTT.js CLI

The CLI takes the transport, host, port and path as separate options. -h is a hostname, not a URL:

# npm i -g mqtt (the MQTT.js CLI)
# Broker mode: matching subscribers get every publish, this client included.
mqtt sub -l wss -h app.hooklistener.com -p 443 --path "/mqtt/<slug>" -u hooklistener -P "<token>" -t "sensors/#" -v
mqtt pub -l wss -h app.hooklistener.com -p 443 --path "/mqtt/<slug>" -u hooklistener -P "<token>" -t "sensors/temp" -m "21.5" -q 1
# Or put the token in the path instead of -u/-P: --path "/mqtt/<slug>?token=<token>"

The REST API and the MCP tools return the connection details in an mqtt object on the endpoint:

{
"url": "wss://app.hooklistener.com/mqtt/<slug>",
"host": "app.hooklistener.com",
"port": 443,
"path": "/mqtt/<slug>",
"transport": "wss",
"websocket_subprotocol": "mqtt",
"protocol_versions": ["3.1", "3.1.1", "5.0"],
"broker": true,
"mode": "broker",
"default_topic": "hooklistener",
"auth": "Send the access token as ?token= in the URL, or as the MQTT CONNECT password (any username)."
}

mode is broker, capture or proxy.

Access token​

Send the access token in the URL (?token=) or a header, or as the CONNECT password with any username. The username also works. See Access token.

When the token travels in CONNECT:

  • No session opens until a CONNECT proves the token.
  • A wrong or missing token gets CONNACK "not authorized" (0x05; MQTT 5 0x87), then the connection is closed with 1008.
  • The refused attempt appears in the console as a closed auth_failed session, with no messages and no credentials.

In proxy mode the CONNECT credentials belong to your broker, so the token must be in the URL or a header.

Connecting​

  • CONNECT → CONNACK. The client id, keep alive, username (never the password), Last Will and MQTT 5 properties are recorded on the session. A client that sends no CONNECT within 30 seconds is closed with 1008.
  • Empty client id. The server assigns one (hl-…), and MQTT 5 clients receive it as the Assigned Client Identifier. MQTT 3.1 and 3.1.1 clients need a clean session for an empty client id.
  • Sessions. In broker mode a client can keep its session between connections, and a client id can be connected only once per endpoint. See Persistent sessions. Capture-only and proxy endpoints keep no sessions: MQTT 5 clients that ask for a Session Expiry Interval are told 0.
  • Not supported: enhanced authentication (CONNECT is refused with 0x8C) and shared subscriptions (MQTT 5 $share/ filters are refused in SUBACK with 0x9E).
  • MQTT 5 topic aliases from the client are resolved, up to 32 per connection.

Broker mode​

Broker mode is on by default (Settings → MQTT broker → Route publishes between clients (broker mode), or mqtt_broker in the API and MCP). In broker mode:

  • Routing. Every PUBLISH is recorded and delivered to each client of the same endpoint with a matching subscription, the publisher included. + matches one level and # the rest. Topics that start with $ are not matched by filters that start with a wildcard.
  • QoS. A message is delivered at the lower of its publish QoS and the QoS the subscription was granted. QoS 1 and 2 deliveries get the full PUBACK, or PUBREC / PUBREL / PUBCOMP, flow.
  • Retained messages. The last retained message on each topic reaches new subscribers. An empty retained payload clears the topic. MQTT 5 Retain Handling and Retain As Published are honoured. Retained messages are kept in memory, so they are lost when the server restarts. Each endpoint keeps up to 1,000 topics and 5 MB of retained payloads; publishes beyond that are delivered but not retained.
  • Last Will. The will is published when a client goes away without a DISCONNECT: a dropped connection, a keep alive timeout, a protocol error or a server close.
  • No Local. MQTT 5 subscriptions with No Local don't receive their own publishes. Subscription identifiers and the publish properties (content type, user properties, response topic, correlation data, payload format, message expiry) are passed on.
  • Fan-out. Deliveries are capped at 2,000 per second per endpoint.

The client's PUBACK or PUBREC goes out only after the message passes the size and rate limits. A message that breaks them is never acknowledged. QoS 2 duplicates are acknowledged but recorded once.

Capture only​

Turn broker mode off ("mqtt_broker": false) to record and acknowledge publishes without delivering them to anyone. Use it to capture what devices send without other clients receiving it. Capture-only endpoints keep no sessions: every session ends with its connection.

Persistent sessions​

In broker mode a client can keep its session while it is disconnected:

  • MQTT 3.1 and 3.1.1: connect with Clean Session off (clean: false in mqtt.js).
  • MQTT 5: connect with a Session Expiry Interval above 0 (properties: { sessionExpiryInterval: 3600 } in mqtt.js).

While the client is away, Hooklistener keeps:

  • its subscriptions, with their granted QoS and MQTT 5 options;
  • the QoS 1 and 2 messages it hadn't acknowledged yet, and its own QoS 2 publishes still waiting for PUBREL;
  • the publishes routed to it while it was offline, in order.

When the same client id reconnects without a clean session:

  1. CONNACK says Session Present 1, and the subscriptions are back. The client doesn't need to subscribe again.
  2. Unacknowledged messages are sent again, as a PUBLISH with DUP set or as a PUBREL.
  3. Then the queued messages are delivered, without ever having more QoS 1 and 2 messages in flight than the client's Receive Maximum (at most 100).

The console shows Session resumed (N queued) on the new session, with the counts of in-flight, dropped and expired messages when there are any.

A clean connect, or a reconnect after the session expired, starts from scratch and discards the stored state.

  • Expiry. A session is kept for up to 24 hours. MQTT 3.1 and 3.1.1 sessions are kept for 24 hours. A longer MQTT 5 Session Expiry Interval is capped, and CONNACK tells the client the capped value. An MQTT 5 client can change the interval in its DISCONNECT. Queued messages whose MQTT 5 Message Expiry Interval runs out are dropped and counted as expired.
  • What is queued. Only publishes the broker routes: other clients' PUBLISHes and Last Wills. Messages you send from the console, the API, MCP, rules or scenarios are not queued for offline clients.
  • Limits. Each endpoint stores up to 50 sessions; when it's full, the session stored the longest ago is dropped. Each session queues up to 1,000 messages and 1 MB. When a queue is full the oldest QoS 0 message is dropped first, then the oldest message of any QoS. Drops are reported when the session resumes.
  • In memory. Stored sessions are lost when the server restarts. The client then gets Session Present 0 and must subscribe again.

Session takeover​

In broker mode a client id is connected once per endpoint. A new connection with a client id that is already connected takes the session over:

  • The old connection is closed with 1000 and the reason MQTT session taken over by a new connection with the same client id. It is recorded as session_taken_over.
  • MQTT 5 clients first get a DISCONNECT with reason code 0x8E (Session taken over).

This applies to clean sessions too. Capture-only and proxy endpoints do no takeover.

On-connect message​

The on-connect payload (Settings → On connect) is published after CONNACK, never before. Its topic is the one you set, or hooklistener when that field is empty or has wildcards.

  • Broker mode: it is held until the client subscribes to a matching topic, then delivered.
  • Capture only and proxy mode: it is published right after CONNACK, whether or not the client subscribed.

Keep alive​

The client's keep alive interval is enforced at 1.5 times its value. A client with keepalive: 60 that sends nothing for 90 seconds is closed with 1001 (MQTT keep alive timeout: nothing received for 90s (1.5 × keep alive 60s)). MQTT 5 clients first get a DISCONNECT with reason code 0x8D. The WebSocket keepalive runs as well.

Sending to clients​

Messages from the console, the REST API, MCP, rules and scenarios are PUBLISHed to the client:

  • Topic. Set the topic in the console composer, or as topic (or event_type) in the API and MCP. A publish needs a concrete topic: wildcards are refused, for example MQTT topic "sensors/#" contains a wildcard (+ or #). Publishes need a concrete topic such as sensors/temp; wildcards are only valid in subscriptions. Without a topic, the client's only plain subscription is used, or else hooklistener.
  • QoS and retain. Choose QoS 0 (the default), 1 or 2 and the retain flag in the composer, or send qos and retain. The message is delivered at the lower of the requested QoS and the QoS the client's matching subscription granted. In broker mode, a retained send with a topic also becomes that topic's retained message, even if no client receives it now.
  • Who receives it. A send to one session is delivered whatever that client subscribed to. In broker mode, a broadcast only reaches clients subscribed to its topic.
  • Before CONNACK nothing is published. A send to a client that hasn't connected yet gets 409 MQTT client has not sent CONNECT yet.
curl -X POST https://app.hooklistener.com/api/v1/realtime/endpoints/<id>/sessions/<session_id>/messages \
-H "Authorization: Bearer hklst_your_api_key" \
-H "Content-Type: application/json" \
-d '{"topic": "devices/42/commands", "payload": "{\"action\":\"reboot\"}", "qos": 1}'

A ping from the console or a scenario is a WebSocket ping, not a PINGREQ.

Rules​

Event name equals matches the topic, and it accepts topic filters: devices/+/request and sensors/# work. A rule reply goes to the topic set on the rule, or to the topic of the message that triggered it. Rules and scenarios can't publish to a topic with wildcards. {{session.client_id}} renders the client id in templates.

With Record ping/pong control frames on, PINGREQ and the QoS acknowledgements are recorded too.

Proxy mode​

In proxy mode your own broker answers the client, and Hooklistener records both directions.

  • Set the Upstream URL to your broker's WebSocket address, for example wss://broker.example.com/mqtt.
  • Packets are relayed byte for byte in both directions. Each PUBLISH is recorded as a message. Other packets are recorded with Record ping/pong control frames on.
  • Broker mode is off: your broker routes messages and keeps sessions. Hooklistener stores no sessions and does no takeover.
  • The client's CONNECT credentials go to your broker. Send the Hooklistener access token in the URL or a header.
  • The on-connect message is published once your broker's CONNACK accepts the client.
  • Messages you inject towards the client are published at QoS 0, because packet ids belong to your broker.

See Proxy mode for headers, credentials and close codes.