# Publish readings over MQTT (https://logable.app/docs/integrate/mqtt)

Connect to the Logable broker over TLS with a device token and publish readings on the device's own topic.



MQTT suits gateways and hardware that holds one connection open instead of opening a new request per reading. The payload is the same JSON body the HTTP endpoint takes.

## Connect [#connect]

Table: see https://logable.app/docs/integrate/mqtt#connect

> **One client ID per connection**

Two clients sharing a client ID disconnect each other, over and over. If you connect a test tool while the device is running, give the tool its own client ID.

Most TLS stacks validate the broker certificate with the roots your operating system already trusts, and need nothing extra. Some gateways ask you to upload a CA file by hand. Give them the ISRG Root X1 certificate from [letsencrypt.org](https://letsencrypt.org/certs/isrgrootx1.pem). Upload the root, never the server certificate, which rotates.

## Publish [#publish]

Publish the reading to the device's own topic at QoS 1.

```text title="Topic"
devices/<DEVICE_ID>/data
```

The message body is the same JSON the HTTP endpoint takes:

```json
{
  "timestamp": "2026-07-01T10:00:00.000Z",
  "status": "online",
  "data": {
    "weight": 18750.5,
    "status": "filling"
  }
}
```

Streams publish to `streams/<STREAM_ID>/data` with a stream token, and their body carries no `status` field.

A message is at most 1 MB.

## What a device is allowed to do [#what-a-device-is-allowed-to-do]

The broker checks permission on every publish and every subscribe, not only when the connection opens. A device may publish and subscribe only under its own namespace, `devices/<its own id>/`. It cannot subscribe to another device's data, and a wildcard subscription across devices is refused.

Logable clears the broker's cached decision when a token is rotated or revoked, so the next publish is checked again. That normally happens within a second, and at most within the broker's cache window. Ingest keeps its own check for up to 30 seconds.

## Code samples [#code-samples]

Generated from the same registry the platform validates against. Replace the placeholder id and token with the values from the device's Connection tab.

#### Python

```python
"""
Raw Meal Silo - Logable MQTT Integration (Python)
Device Type: silo
Device ID: <DEVICE_ID>

Broker: MQTT over TLS on port 8883 (Let's Encrypt certificate; standard system CAs validate it). WebSocket: wss://mqtt.logable.app/mqtt
Client ID must be unique per connection - a duplicate ID disconnects the other client.
Access: this device may publish/subscribe only under devices/<DEVICE_ID>/* - everything else is denied.

Installation: pip install paho-mqtt
"""

import paho.mqtt.client as mqtt
import json
from datetime import datetime, timezone
import time

MQTT_HOST = "mqtt.logable.app"
MQTT_PORT = 8883
MQTT_USERNAME = "<DEVICE_ID>"
MQTT_PASSWORD = "<DEVICE_TOKEN>"
MQTT_CLIENT_ID = "logable-<DEVICE_ID>"
MQTT_TOPIC = "devices/<DEVICE_ID>/data"

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"Connection failed: {reason_code}")
    else:
        print(f"Connected to MQTT broker at {MQTT_HOST}:{MQTT_PORT}")

def send_data(client):
    # The "data" fields shown are this device type's registered metrics.
    # Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
    # Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
    # Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
    # Custom values appear on dashboards via a widget's "Show Custom Data" option.
    payload = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "status": "online",
        "data": {
            "weight": 18750.5,
            "status": "filling"
        }
    }
    result = client.publish(MQTT_TOPIC, json.dumps(payload), qos=1)
    if result.rc == mqtt.MQTT_ERR_SUCCESS:
        print(f"Data sent to {MQTT_TOPIC}")

def main():
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=MQTT_CLIENT_ID)
    client.on_connect = on_connect
    client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
    # TLS with the default system CA store - validates the broker's
    # Let's Encrypt certificate out of the box.
    client.tls_set()

    try:
        client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
        client.loop_start()
        time.sleep(1)
        send_data(client)
        time.sleep(1)
        client.loop_stop()
        client.disconnect()
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()
```

#### TypeScript

```typescript
/**
 * Raw Meal Silo - Logable MQTT Integration (TypeScript)
 * Device Type: silo
 * Device ID: <DEVICE_ID>
 *
 * Broker: MQTT over TLS on port 8883 (Let's Encrypt certificate; standard system CAs validate it). WebSocket: wss://mqtt.logable.app/mqtt
 * Client ID must be unique per connection - a duplicate ID disconnects the other client.
 * Access: this device may publish/subscribe only under devices/<DEVICE_ID>/* - everything else is denied.
 *
 * Installation: npm install mqtt
 */

import mqtt from 'mqtt';

// mqtts:// - the mqtt package handles TLS and CA validation automatically.
const MQTT_URL = "mqtts://mqtt.logable.app:8883";
const MQTT_USERNAME = "<DEVICE_ID>";
const MQTT_PASSWORD = "<DEVICE_TOKEN>";
const MQTT_CLIENT_ID = "logable-<DEVICE_ID>";
const MQTT_TOPIC = "devices/<DEVICE_ID>/data";

async function sendData(): Promise<void> {
  const client = mqtt.connect(MQTT_URL, {
    clientId: MQTT_CLIENT_ID,
    username: MQTT_USERNAME,
    password: MQTT_PASSWORD,
    clean: true,
  });

  client.on('connect', () => {
    console.log(`Connected to MQTT broker at ${MQTT_URL}`);

    // The "data" fields shown are this device type's registered metrics.
    // Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
    // Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
    // Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
    // Custom values appear on dashboards via a widget's "Show Custom Data" option.
    const payload = {
      timestamp: new Date().toISOString(),
      status: "online",
      data: {
      "weight": 18750.5,
      "status": "filling"
      },
    };

    client.publish(
      MQTT_TOPIC,
      JSON.stringify(payload),
      { qos: 1 },
      (error) => {
        if (error) console.error('Failed to publish:', error);
        else console.log(`Data sent to ${MQTT_TOPIC}`);
        client.end();
      }
    );
  });

  client.on('error', (error) => {
    console.error('MQTT error:', error);
    client.end();
  });
}

sendData();
```

#### Bash

```bash
#!/bin/bash
# Raw Meal Silo - Logable MQTT Integration (Bash)
# Device Type: silo
# Device ID: <DEVICE_ID>
#
# Broker: MQTT over TLS on port 8883 (Let's Encrypt certificate; standard system CAs validate it). WebSocket: wss://mqtt.logable.app/mqtt
# Client ID must be unique per connection - a duplicate ID disconnects the other client.
# Access: this device may publish/subscribe only under devices/<DEVICE_ID>/* - everything else is denied.
#
# Requirements: mosquitto-clients

MQTT_HOST="mqtt.logable.app"
MQTT_PORT=8883
MQTT_USERNAME="<DEVICE_ID>"
MQTT_PASSWORD="<DEVICE_TOKEN>"
MQTT_CLIENT_ID="logable-<DEVICE_ID>"
MQTT_TOPIC="devices/<DEVICE_ID>/data"

# NOTE: the heredoc below is pure JSON, so it cannot contain comments.
# The "data" fields shown are this device type's registered metrics.
# Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
# Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
# Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
# Custom values appear on dashboards via a widget's "Show Custom Data" option.
# Quoted heredoc delimiter: '$' in metric values is taken literally.
# "timestamp" is optional - the platform stamps arrival time when omitted.
read -r -d '' PAYLOAD <<'EOF'
{
  "status": "online",
  "data": {
    "weight": 18750.5,
    "status": "filling"
  }
}
EOF

# TLS: --capath works on Linux; on macOS use --cafile /etc/ssl/cert.pem instead.
mosquitto_pub \
  -h "${MQTT_HOST}" \
  -p "${MQTT_PORT}" \
  --capath /etc/ssl/certs \
  -i "${MQTT_CLIENT_ID}" \
  -u "${MQTT_USERNAME}" \
  -P "${MQTT_PASSWORD}" \
  -t "${MQTT_TOPIC}" \
  -m "${PAYLOAD}" \
  -q 1

if [ $? -eq 0 ]; then
  echo "Data published to ${MQTT_TOPIC}"
  exit 0
else
  echo "Failed to publish"
  exit 1
fi
```

#### PowerShell

```powershell
<#
.SYNOPSIS
Raw Meal Silo - Logable MQTT Integration (PowerShell)
Device Type: silo
Device ID: <DEVICE_ID>

Broker: MQTT over TLS on port 8883 (Let's Encrypt certificate; standard system CAs validate it). WebSocket: wss://mqtt.logable.app/mqtt
Client ID must be unique per connection - a duplicate ID disconnects the other client.
Access: this device may publish/subscribe only under devices/<DEVICE_ID>/* - everything else is denied.

Requirements: the M2Mqtt .NET library (M2Mqtt.Net.dll).
Install via NuGet: Install-Package M2Mqtt
(or download the package and extract lib/net45/M2Mqtt.Net.dll,
then adjust the Add-Type path below)
#>

$MqttHost = "mqtt.logable.app"
$MqttPort = 8883
$MqttUsername = "<DEVICE_ID>"
$MqttPassword = "<DEVICE_TOKEN>"
$MqttClientId = "logable-<DEVICE_ID>"
$MqttTopic = "devices/<DEVICE_ID>/data"

Add-Type -Path "M2Mqtt.Net.dll"

function Send-DeviceDataMqtt {
    $Timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")

    # The "data" fields shown are this device type's registered metrics.
    # Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
    # Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
    # Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
    # Custom values appear on dashboards via a widget's "Show Custom Data" option.
    $Payload = @{
        timestamp = $Timestamp
        status = "online"
        data = @{
            weight = 18750.5
            status = 'filling'
        }
    } | ConvertTo-Json -Depth 3

    # TLS on the standard MQTT-over-TLS port; .NET validates the
    # broker's Let's Encrypt certificate against the system store.
    $Client = [uPLibrary.Networking.M2Mqtt.MqttClient]::new(
        $MqttHost, $MqttPort, $true,
        [uPLibrary.Networking.M2Mqtt.MqttSslProtocols]::TLSv1_2,
        $null, $null
    )

    try {
        $ReturnCode = $Client.Connect($MqttClientId, $MqttUsername, $MqttPassword)
        if ($ReturnCode -ne 0) {
            throw "MQTT connect failed (code $ReturnCode)"
        }

        $Client.Publish(
            $MqttTopic,
            [System.Text.Encoding]::UTF8.GetBytes($Payload),
            [uPLibrary.Networking.M2Mqtt.Messages.MqttMsgBase]::QOS_LEVEL_AT_LEAST_ONCE,
            $false
        ) | Out-Null

        Write-Host "Data published to $MqttTopic" -ForegroundColor Green
    } catch {
        Write-Error "Error: $($_.Exception.Message)"
        throw
    } finally {
        if ($Client.IsConnected) {
            $Client.Disconnect()
        }
    }
}

Send-DeviceDataMqtt
```

#### C (Paho MQTT)

```c
/*
 * Raw Meal Silo - Logable MQTT Integration (C / Eclipse Paho)
 * Device Type: silo
 * Device ID: <DEVICE_ID>
 *
 * Broker: MQTT over TLS on port 8883 (Let's Encrypt certificate; standard system CAs validate it). WebSocket: wss://mqtt.logable.app/mqtt
 * Client ID must be unique per connection - a duplicate ID disconnects the other client.
 * Access: this device may publish/subscribe only under devices/<DEVICE_ID>/* - everything else is denied.
 *
 * Install: sudo apt-get install libpaho-mqtt-dev
 * Compile: gcc -o mqtt_client mqtt_client.c -lpaho-mqtt3cs
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <MQTTClient.h>

#define MQTT_ADDRESS "ssl://mqtt.logable.app:8883"
#define MQTT_CLIENT_ID "logable-<DEVICE_ID>"
#define MQTT_USERNAME "<DEVICE_ID>"
#define MQTT_PASSWORD "<DEVICE_TOKEN>"
#define MQTT_TOPIC "devices/<DEVICE_ID>/data"
#define MQTT_QOS 1
#define MQTT_TIMEOUT_MS 10000L

/* Example metric payload - CUSTOMIZE DATA HERE */
static const char *DATA_JSON =
    "{\"weight\":18750.5,\"status\":\"filling\"}";

int main(void) {
    MQTTClient client;
    MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
    MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer;
    MQTTClient_message pubmsg = MQTTClient_message_initializer;
    MQTTClient_deliveryToken token;
    char payload[1024];
    char timestamp[64];
    time_t now = time(NULL);
    int rc;

    strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));

    /* Build JSON payload - CUSTOMIZE DATA HERE */
    /*
     * The "data" fields shown are this device type's registered metrics.
     * Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
     * Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
     * Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
     * Custom values appear on dashboards via a widget's "Show Custom Data" option.
     */
    /* Metric data is passed as a %s argument (never spliced into the
     * format string), so '%' characters in values stay intact. */
    snprintf(payload, sizeof(payload),
        "{\"timestamp\": \"%s\", \"status\": \"online\", \"data\": %s}",
        timestamp, DATA_JSON);

    rc = MQTTClient_create(&client, MQTT_ADDRESS, MQTT_CLIENT_ID,
        MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "Failed to create client (rc=%d)\n", rc);
        return 1;
    }

    conn_opts.keepAliveInterval = 60;
    conn_opts.cleansession = 1;
    conn_opts.username = MQTT_USERNAME;
    conn_opts.password = MQTT_PASSWORD;
    /* TLS: validate the broker's Let's Encrypt certificate against the
     * system CA store (works out of the box on Linux/macOS). */
    ssl_opts.enableServerCertAuth = 1;
    conn_opts.ssl = &ssl_opts;

    rc = MQTTClient_connect(client, &conn_opts);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "Failed to connect (rc=%d)\n", rc);
        MQTTClient_destroy(&client);
        return 1;
    }
    printf("Connected to MQTT broker\n");

    pubmsg.payload = payload;
    pubmsg.payloadlen = (int)strlen(payload);
    pubmsg.qos = MQTT_QOS;
    pubmsg.retained = 0;

    rc = MQTTClient_publishMessage(client, MQTT_TOPIC, &pubmsg, &token);
    if (rc == MQTTCLIENT_SUCCESS) {
        rc = MQTTClient_waitForCompletion(client, token, MQTT_TIMEOUT_MS);
        printf("Data published to %s\n", MQTT_TOPIC);
    } else {
        fprintf(stderr, "Failed to publish (rc=%d)\n", rc);
    }

    MQTTClient_disconnect(client, 1000);
    MQTTClient_destroy(&client);
    return 0;
}
```

#### Arduino / ESP32

```cpp
/*
 * Raw Meal Silo - Logable MQTT Integration (Arduino / ESP32)
 * Device Type: silo
 * Device ID: <DEVICE_ID>
 *
 * Broker: MQTT over TLS on port 8883 (Let's Encrypt certificate; standard system CAs validate it). WebSocket: wss://mqtt.logable.app/mqtt
 * Client ID must be unique per connection - a duplicate ID disconnects the other client.
 * Access: this device may publish/subscribe only under devices/<DEVICE_ID>/* - everything else is denied.
 *
 * Libraries: WiFi, WiFiClientSecure, PubSubClient, ArduinoJson
 * Board: ESP32 or ESP8266
 */

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "mqtt.logable.app";
const int MQTT_PORT = 8883;
const char* MQTT_USERNAME = "<DEVICE_ID>";
const char* MQTT_PASSWORD = "<DEVICE_TOKEN>";
const char* MQTT_CLIENT_ID = "logable-<DEVICE_ID>";
const char* MQTT_TOPIC = "devices/<DEVICE_ID>/data";

WiFiClientSecure espClient;
PubSubClient mqttClient(espClient);

void setupWifi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" Connected!");
}

void reconnectMqtt() {
  while (!mqttClient.connected()) {
    Serial.print("Connecting to MQTT...");
    if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
      Serial.println(" Connected!");
    } else {
      Serial.printf(" Failed (rc=%d). Retrying in 5s...\n", mqttClient.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  setupWifi();

  /* TLS: setInsecure() skips certificate validation - acceptable for a
   * quick test, but production firmware should pin the ISRG Root X1 CA
   * (Let's Encrypt root) instead:
   *
   *   static const char ISRG_ROOT_X1[] PROGMEM = R"CERT(
   *   -----BEGIN CERTIFICATE-----
   *   ...paste the ISRG Root X1 PEM from https://letsencrypt.org/certificates/
   *   -----END CERTIFICATE-----
   *   )CERT";
   *   espClient.setCACert(ISRG_ROOT_X1);
   */
  espClient.setInsecure();

  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
  /* Must be set BEFORE connecting - PubSubClient's 256-byte default
   * truncates typical Logable payloads. Keep payloads under 512 bytes. */
  mqttClient.setBufferSize(512);
}

void sendMetrics() {
  if (!mqttClient.connected()) {
    reconnectMqtt();
  }
  mqttClient.loop();

  /* Build JSON payload - CUSTOMIZE DATA HERE (keep under 512 bytes) */
  JsonDocument doc;
  doc["status"] = "online";
  doc.createNestedObject("data");
  doc["data"]["weight"] = 18750.5;
  doc["data"]["status"] = "filling";
  // The "data" fields shown are this device type's registered metrics.
  // Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
  // Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
  // Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
  // Custom values appear on dashboards via a widget's "Show Custom Data" option.

  char payload[512];
  serializeJson(doc, payload, sizeof(payload));

  if (mqttClient.publish(MQTT_TOPIC, payload)) {
    Serial.println("Data published via MQTT");
  } else {
    Serial.println("MQTT publish failed");
  }
}

void loop() {
  sendMetrics();
  delay(10000); /* Send every 10 seconds */
}
```

## Validation and limits [#validation-and-limits]

Everything the HTTP endpoint enforces applies here too, because both transports meet at the same validation step: the payload limits, the custom key rules, and the one hour maximum drift on a supplied timestamp.

The rate limit is the same on both transports: 10 messages per second per device. Over MQTT a message above the limit is dropped without a reply. The device log shows the drop.

## The device log [#the-device-log]

MQTT has no reply channel, so a device never learns why a message was refused. The device log shows you the refusal instead. Open a device, go to the Connection tab, and the log shows what the device sent, what Logable accepted, what it rejected and why, and what the broker saw when the device connected or dropped.

It shows the wrong topic, invalid JSON, a metric of the wrong type, a payload over the limit, a burst over the rate limit, a token the broker refused, and a publish outside the device namespace. Every row names the fix. See [Error codes](/docs/reference/error-codes) for the full list.

The log is live. It shows what happens while it is open, plus the one most recent problem, which also appears as a line on the device overview until the device sends an accepted message.

## Going offline [#going-offline]

Logable decides a device is offline from its readings, not from the broker connection. A device is marked offline when it stops sending. The wait is five minutes, or three times the device's own reporting interval when that is longer.

Keep publishing on a steady interval even when nothing changes. A silence is how Logable knows something is wrong.

## Gateways [#gateways]

A PLC or a Modbus device does not speak MQTT. A gateway sits between them: it reads the equipment over Modbus RTU or Modbus TCP and republishes each reading on the topic above. Logable sees a normal MQTT device. Every gateway that can publish JSON to a topic with a username and password works, and each piece of equipment behind the gateway gets its own Logable device, its own token and its own topic.

## Read next [#read-next]

* [Send readings over HTTP](/docs/integrate/rest-api)
* [Device types](/docs/reference/device-types)
* [Get started with Logable](/docs/guides/getting-started)
* [Error codes](/docs/reference/error-codes)
