TagoTiP over MQTT: Complete Device Setup Tutorial

You have a microcontroller, sensor data to send, and a TagoIO account. This tutorial walks you through every step — from creating your first device in the platform to seeing data arrive on your dashboard. By the end, you’ll have a working MQTT connection with proper authentication, structured data payloads, and reliable connectivity.

What you’ll build: An ESP32 device that connects to TagoIO via MQTT, sends sensor readings at regular intervals, stays connected with proper keepalive, and listens for server commands.

What you’ll need:

  • A TagoIO account (Free tier works)
  • An ESP32 board (or any MQTT-capable device)
  • Arduino IDE with the PubSubClient library installed
  • 10 minutes

Step 1: Create Your Device in TagoIO

Every device on TagoIO needs a record in the platform. This record maps your physical hardware to a virtual identity using a serial number. The device stores all incoming data directly — no separate configuration needed.

  1. Open Devices in your TagoIO admin panel.
  2. Click Add Device.
  3. Select the network TagoTiP MQTT and choose the TagoTiP over MQTT connector.
  4. Assign a Serial Number — for example, sensor-01.

This serial becomes part of your MQTT topic path. Choose something meaningful: a MAC address, asset tag, or project-specific identifier.

Tip: Serial numbers can be up to 100 characters. Keep them lowercase and avoid special characters beyond hyphens and underscores for cleaner topic paths.

See the Quick Start guide →


Step 2: Generate an Authorization

The authorization provides the credentials your device needs to authenticate over MQTT.

  1. Go to Devices > Authorization (tab at the top of the Devices page).
  2. Click Generate.
  3. Set a descriptive name (e.g., “Production sensors auth”).
  4. Select the token format TagoTiP(s) MQTT.
  5. Click Generate.

You’ll receive two credentials:

Credential Format Purpose
Token Hash 4deedd7bab8817ec (16 hex chars) Used for MQTT authentication
Token ate2bd...c0d0 (34 chars) Used only for TagoTiP(s) encrypted mode — keep this secret

For MQTT, you only need the Token Hash. Save it now — you won’t see it again.

Important: One authorization can serve multiple devices. All devices sharing the same credentials belong to the same “context” and can see each other’s topics. If you need device isolation, generate separate authorizations.

See the Authorization guide →


Step 3: Understand the MQTT Authentication

TagoTiP over MQTT splits your 16-character Token Hash into the standard MQTT username and password fields:

MQTT Field Value Example
Username First 8 hex characters 4deedd7b
Password Last 8 hex characters ab8817ec

The server reconstructs the full hash by concatenating both halves. No extra encoding, no bearer prefix — just the raw hex characters.

Why split it this way? MQTT’s CONNECT packet has dedicated username/password fields. Splitting the hash avoids wasting bytes on a custom auth topic or payload field, and lets any standard MQTT client library connect without modification.


Step 4: Understand the Topic Structure

TagoTiP uses three topics per device, all under the $tip/ prefix:

Topic Direction Purpose
$tip/{serial}/push Device → Server Send sensor data
$tip/{serial}/pull Device → Server Request stored values
$tip/{serial}/ack Server → Device Responses and commands

For our sensor-01 device, the topics are:

  • $tip/sensor-01/push
  • $tip/sensor-01/pull
  • $tip/sensor-01/ack

Your device must subscribe to the ack topic immediately after connecting. This is how it receives acknowledgments and server-pushed commands.


Step 5: Choose Your Endpoint and Port

Connect to the region closest to your deployment:

Region Host Ports
US-East-1 mqtt.tip.us-e1.tago.io 1883 (MQTT) / 8883 (MQTTS)
EU-West-1 mqtt.tip.eu-w1.tago.io 1883 (MQTT) / 8883 (MQTTS)

Use port 8883 (TLS) for production. Port 1883 is fine for development, but data travels unencrypted. If you must use port 1883 in production, consider the $tips/ prefix for application-layer encryption (covered in the security section below).

See all endpoints →


Step 6: Test with Mosquitto (Before Writing Code)

Before touching embedded code, confirm your device and credentials work. Install mosquitto and run:

mosquitto_rr -h mqtt.tip.us-e1.tago.io -p 1883 \
  -u 4deedd7b -P ab8817ec \
  -t '$tip/sensor-01/push' -e '$tip/sensor-01/ack' \
  -m '[temperature:=25.5#C]'

Expected response:

OK|1

That OK|1 means one data point was stored in your device. If you see an error instead, check the troubleshooting section below.

Tip: mosquitto_rr is a “request-response” tool — it publishes to the push topic and waits for a reply on the ack topic in a single command. It’s the fastest way to validate your setup.

Push multiple variables at once

mosquitto_rr -h mqtt.tip.us-e1.tago.io -p 1883 \
  -u 4deedd7b -P ab8817ec \
  -t '$tip/sensor-01/push' -e '$tip/sensor-01/ack' \
  -m '[temperature:=25.5#C;humidity:=60#%;battery:=3.7#V]'

Pull last stored values

mosquitto_rr -h mqtt.tip.us-e1.tago.io -p 1883 \
  -u 4deedd7b -P ab8817ec \
  -t '$tip/sensor-01/pull' -e '$tip/sensor-01/ack' \
  -m 'temperature,humidity'

Test with TLS (port 8883)

mosquitto_rr -h mqtt.tip.us-e1.tago.io -p 8883 --capath /etc/ssl/certs \
  -u 4deedd7b -P ab8817ec \
  -t '$tip/sensor-01/push' -e '$tip/sensor-01/ack' \
  -m '[temperature:=25.5#C]'

Step 7: Understand the Payload Format

Over MQTT, you only send the body — no method, no hash, no serial (those are already in the topic and credentials). The payload syntax looks like this:

[variable_name:=value#unit;another_var:=value#unit]

Type operators

Operator Type Example
:= Number temperature:=25.5
= String status=online
?= Boolean active?=true
@= Location (lat,lng) position@=39.74,-104.99

Optional suffixes

You can attach unit, location, timestamp, group, and metadata to any variable:

temperature:=25.5#C@=39.74,-104.99@1694567890000^batch_01{source=dht22}

The order is: value → unit (#) → location (@=) → timestamp (@) → group (^) → metadata ({}).

Sequence counter (optional)

Prefix your payload with !N| to correlate responses:

!42|[temperature:=25.5#C]

The server echoes it back: !42|OK|1. This is useful when you send multiple messages in quick succession and need to match each response to its request.

Raw/binary payloads (passthrough)

If your device sends binary, hex-encoded, or proprietary data instead of structured TagoTiP variables, use the passthrough prefix:

>xDEADBEEF01020304     (hex-encoded bytes)
>b3q2+7wECAwQ=          (base64-encoded bytes)

The raw bytes are delivered directly to your device’s Payload Parser (also called Connector Decoder) for decoding. This is the recommended approach when:

  • Your sensor outputs a proprietary binary format
  • You’re migrating from LoRaWAN/Sigfox where payloads are hex-encoded
  • You want to keep the embedded code minimal and handle decoding server-side

To decode raw payloads, write a Payload Parser in your device’s settings. The parser transforms the binary data into named variables before storage.

See the Payload Parser guide →
MQTT Payload Parser tutorial →
Building your own parser →


Step 8: Write the Embedded Code (ESP32 + Arduino)

Here’s a complete, production-ready sketch. It connects to WiFi, authenticates with TagoIO over MQTT, sends temperature readings every 10 seconds, and handles reconnection and server commands.

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

// === CONFIGURATION ===
const char* WIFI_SSID     = "your-wifi";
const char* WIFI_PASSWORD = "your-password";

const char* MQTT_HOST  = "mqtt.tip.us-e1.tago.io";
const int   MQTT_PORT  = 1883;
const char* MQTT_USER  = "4deedd7b";  // first 8 hex chars of your token hash
const char* MQTT_PASS  = "ab8817ec";  // last 8 hex chars of your token hash
const char* SERIAL_N   = "sensor-01"; // must match the serial in TagoIO

// === TOPICS ===
char pushTopic[64];
char ackTopic[64];

// === MQTT CLIENT ===
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);

// === CALLBACK: Handle responses and commands ===
void onMessage(char* topic, byte* payload, unsigned int length) {
  char msg[256];
  unsigned int copyLen = min(length, (unsigned int)(sizeof(msg) - 1));
  memcpy(msg, payload, copyLen);
  msg[copyLen] = '\0';

  Serial.print("[ACK] ");
  Serial.println(msg);

  // Handle server commands
  if (strncmp(msg, "CMD|", 4) == 0) {
    String command = String(msg + 4);
    Serial.print("[CMD] Received: ");
    Serial.println(command);

    if (command == "reboot") {
      Serial.println("Rebooting...");
      delay(1000);
      ESP.restart();
    }
  }

  // Handle errors
  if (strncmp(msg, "ERR|", 4) == 0) {
    Serial.print("[ERR] Server error: ");
    Serial.println(msg + 4);
  }
}

// === WIFI CONNECTION ===
void connectWiFi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" connected");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
}

// === MQTT CONNECTION ===
void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("Connecting to MQTT...");

    // Use a unique client ID (MAC-based is a good choice)
    String clientId = "esp32-" + String(WiFi.macAddress());

    if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
      Serial.println(" connected");

      // Subscribe to ack topic immediately — this is required
      mqtt.subscribe(ackTopic, 1);
      Serial.print("Subscribed to: ");
      Serial.println(ackTopic);
    } else {
      Serial.print(" failed (rc=");
      Serial.print(mqtt.state());
      Serial.println("). Retrying in 5s...");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  delay(100);

  // Build topic strings from serial
  snprintf(pushTopic, sizeof(pushTopic), "$tip/%s/push", SERIAL_N);
  snprintf(ackTopic,  sizeof(ackTopic),  "$tip/%s/ack",  SERIAL_N);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setKeepAlive(4);  // PINGREQ every 4 seconds (must be < 5s idle timeout)

  connectMQTT();
}

void loop() {
  // Reconnect if needed
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }
  if (!mqtt.connected()) {
    connectMQTT();
  }

  // Process incoming messages and maintain keepalive
  mqtt.loop();

  // Send data every 10 seconds
  static unsigned long lastSend = 0;
  if (millis() - lastSend >= 10000) {
    lastSend = millis();

    // Read your sensor (example: analog reading)
    float temperature = analogRead(34) * 0.1;
    float humidity = analogRead(35) * 0.2;

    // Build TagoTiP payload
    char payload[128];
    snprintf(payload, sizeof(payload),
      "[temperature:=%.1f#C;humidity:=%.1f#%%]",
      temperature, humidity);

    // Publish to push topic
    if (mqtt.publish(pushTopic, payload, false)) {
      Serial.print("[PUSH] ");
      Serial.println(payload);
    } else {
      Serial.println("[PUSH] Failed to publish");
    }
  }
}

Step 9: Keepalive and Heartbeats

Keepalive is handled natively by the MQTT protocol. You don’t need to implement a custom heartbeat or PING at the application layer — MQTT’s built-in PINGREQ/PINGRESP mechanism does this for you automatically.

How it works

When you set a keep-alive interval in your MQTT client, the library sends PINGREQ packets to the server whenever the connection is idle. The server responds with PINGRESP. If the server doesn’t receive any traffic (data or PINGREQ) within its idle timeout, it drops the connection.

TagoIO enforces two timers:

Timer Value What it means
Keep-alive idle timeout 5 seconds If no traffic flows for 5s, the server drops the connection
Connection TTL 10–15 seconds (plan-dependent) Maximum total connection lifetime

What you need to do

Set your MQTT keep-alive interval below the 5-second idle timeout:

mqtt.setKeepAlive(4);  // PubSubClient handles PINGREQ automatically

Then call mqtt.loop() frequently in your main loop — at least every second. The library sends PINGREQ for you. That’s it. No custom heartbeat code needed.

Reconnection is expected

Because the Connection TTL is short (10–15s), your device will get disconnected periodically. This is by design — TagoTiP is optimized for efficient, short-lived sessions. Build reconnection into your main loop:

void loop() {
  if (!mqtt.connected()) {
    connectMQTT();  // Reconnects and re-subscribes
  }
  mqtt.loop();
  // ... send data ...
}

Tip: For battery-powered devices, this short TTL is an advantage. Connect, send your batch, disconnect, sleep. No wasted power maintaining idle connections.


Step 10: Send Data with Rich Metadata

Here are common payload patterns you’ll use in practice:

Basic sensor reading with unit

[temperature:=25.5#C]

Multiple variables in one publish

[temperature:=25.5#C;humidity:=60#%;pressure:=1013.25#hPa]

GPS position

[position@=39.74,-104.99,1609]

Variable with timestamp (for batched/historical data)

[temperature:=25.5#C@1694567890000]

The timestamp is in milliseconds since Unix epoch.

Variable with metadata

[temperature:=25.5#C{sensor=dht22,firmware=2.1}]

Full example: location + timestamp + group + metadata

[temperature:=25.5#C@=39.74,-104.99@1694567890000^batch_01{source=dht22}]

Raw/binary passthrough

For devices that send hex-encoded or base64-encoded binary data:

>xDEADBEEF01020304
>b3q2+7wECAwQ=

These arrive at your device’s Payload Parser for decoding into named variables.


Step 11: Receive Server Commands

When you subscribe to $tip/{serial}/ack, you receive both data acknowledgments and server-pushed commands. Commands arrive as:

CMD|reboot
CMD|ota=https://example.com/v2.1.bin
CMD|set_interval=5000

In your callback, parse commands and act on them:

if (strncmp(msg, "CMD|", 4) == 0) {
  char* command = msg + 4;

  if (strcmp(command, "reboot") == 0) {
    ESP.restart();
  }
  else if (strncmp(command, "ota=", 4) == 0) {
    char* url = command + 4;
    // Start OTA update from URL
  }
  else if (strncmp(command, "set_interval=", 13) == 0) {
    int interval = atoi(command + 13);
    // Update send interval
  }
}

Commands are delivered asynchronously — they arrive on the ack topic the moment they’re queued, as long as your connection is active and subscribed.


Step 12: Enable TLS for Production

For production deployments, switch to port 8883:

#include <WiFiClientSecure.h>

WiFiClientSecure wifiClient;
PubSubClient mqtt(wifiClient);

// In setup:
wifiClient.setInsecure();  // Or load a CA certificate for full verification
mqtt.setServer("mqtt.tip.us-e1.tago.io", 8883);

For proper certificate verification, load the CA certificate:

const char* ca_cert = R"(
-----BEGIN CERTIFICATE-----
... your CA cert here ...
-----END CERTIFICATE-----
)";

wifiClient.setCACert(ca_cert);

Alternative: TagoTiP/S (application-layer encryption)

If TLS is too expensive for your device (RAM, handshake time), use the $tips/ topic prefix instead of $tip/. This activates TagoTiP’s built-in AEAD encryption at the application layer:

snprintf(pushTopic, sizeof(pushTopic), "$tips/%s/push", SERIAL_N);
snprintf(ackTopic,  sizeof(ackTopic),  "$tips/%s/ack",  SERIAL_N);

You can enforce encrypted-only communication by setting the device’s Protocol to “TagoTips only” in the TagoIO admin panel. The server will reject any plaintext traffic on $tip/ topics for that device.

See the encryption guide →


Troubleshooting

ERR|invalid_token

  • Double-check that your username is the first 8 characters and your password is the last 8 characters of the Token Hash.
  • Confirm the authorization hasn’t been deleted or regenerated.
  • Make sure you generated a TagoTiP(s) format token, not a standard device token.

ERR|device_not_found

  • The serial in your topic path must exactly match the serial you assigned when creating the device. Case-sensitive.
  • Verify the device is under the same account/profile as the authorization.

ERR|invalid_payload

  • Check your bracket syntax: [variable:=value] — the square brackets are required.
  • Confirm you’re using the correct operator (:= for numbers, = for strings, ?= for booleans).
  • Variable names can’t contain spaces or special characters beyond underscores and hyphens.
  • If sending raw/binary data, make sure you prefix with >x (hex) or >b (base64).

ERR|rate_limited

  • Back off and retry. Check your plan’s rate limits in the documentation.

Connection drops immediately

  • Set your MQTT keep-alive interval to 4 seconds or less.
  • Call mqtt.loop() frequently in your main loop — at least every second.
  • Remember the Connection TTL is 10–15 seconds. Reconnection is expected behavior.

No response on the ack topic

  • Confirm you subscribed to $tip/{serial}/ack with QoS 1.
  • Subscribe immediately after mqtt.connect() returns true — before publishing anything.

PubSubClient rc=-2 (connection timeout)

  • Verify your device can resolve DNS. Try pinging mqtt.tip.us-e1.tago.io from another device on the same network.
  • Check your firewall allows outbound traffic on port 1883 (or 8883 for TLS).

Hex/Binary raw payload not being decoded

  • Confirm you have a Payload Parser enabled on your device in TagoIO.
  • Use the Live Inspector (in the device’s settings) to see the raw payload arriving and trace parser errors.
  • Check the parser code by sending test payloads via the Device Emulator.

See the Live Inspector documentation →


Rate Limits and Supported Features

TagoTiP over MQTT has specific rate limits, connection caps, and feature support that vary by plan. Refer to the official documentation:

See MQTT rate limits and supported features →

Key points to keep in mind:

  • The keep-alive idle timeout is 5 seconds across all plans.
  • The Connection TTL ranges from 10s (Free/Starter) to 15s (Scale).
  • Features like Retain, Last Will, Persistent Sessions, and Offline Messages are not supported.
  • MQTT versions 3.1 and 5 are both supported, with QoS 0, 1, and 2.

What’s Next

You have data flowing from your device to TagoIO. From here you can:

  • Build a dashboard to visualize your sensor data in real time.
  • Create alerts with Actions that trigger when values cross thresholds.
  • Add a Payload Parser to decode raw binary payloads into structured variables.
  • Automate provisioning with a Dashboard + Analysis workflow to onboard devices at scale.
  • Scale to more devices using the same authorization — just assign unique serials.

Explore the TagoTiP specification →

Browse TagoTiP SDKs (Arduino, Python, Go, Node.js, Rust) →


Documentation links: TagoTiP over MQTT | Quick Start Setup | Endpoints | Payload Parser | Device Management API

1 Like