Docs

One topic per device, one versioned JSON envelope, and a REST API described by OpenAPI. The whole contract fits on this page.

Quick start

You need a beta account. Ask for one if you do not have it yet.

  1. Sign in

    Open the dashboard.

  2. Create an organization

    A default project is created with it.

  3. Add a device

    Its client ID and topic are generated.

  4. Create a credential

    Copy the secret; it is shown once.

  5. Publish

    Use a sample below and watch the device page.

Code samples

Replace DEVICE_ID, CREDENTIAL_ID, SECRET, and TOPIC with the values from the device page. The broker certificate is publicly trusted, so the system CA store is enough.

mosquitto_pub -h mqtt.iotaps.com -p 8883 --capath /etc/ssl/certs \
  -V mqttv5 -q 1 -i "$DEVICE_ID" -u "$CREDENTIAL_ID" -P "$SECRET" \
  -t "$TOPIC" \
  -m "{\"schema_version\":1,\"message_id\":\"$(uuidgen)\",
       \"metrics\":{\"temperature_c\":21.5}}"
import json, time, uuid
import paho.mqtt.client as mqtt  # pip install paho-mqtt (2.x)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=DEVICE_ID, protocol=mqtt.MQTTv5)
client.username_pw_set(CREDENTIAL_ID, SECRET)
client.tls_set()  # system CA store, hostname verification on
client.connect("mqtt.iotaps.com", 8883, keepalive=60)
client.loop_start()

while True:
    message = {
        "schema_version": 1,
        "message_id": str(uuid.uuid4()),  # reuse only when retrying
        "metrics": {"temperature_c": 21.5, "fan_on": True},
    }
    client.publish(TOPIC, json.dumps(message), qos=1).wait_for_publish()
    time.sleep(10)
import mqtt from "mqtt";  // npm install mqtt (5.x)
import { randomUUID } from "node:crypto";

const client = mqtt.connect("mqtts://mqtt.iotaps.com:8883", {
  clientId: DEVICE_ID,
  username: CREDENTIAL_ID,
  password: SECRET,
  protocolVersion: 5,
});

client.on("connect", () => {
  setInterval(() => {
    const message = {
      schema_version: 1,
      message_id: randomUUID(),
      metrics: { temperature_c: 21.5 },
    };
    client.publish(TOPIC, JSON.stringify(message), { qos: 1 });
  }, 10_000);
});
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>  // MQTT 3.1.1, publishes with QoS 0

// ISRG Root X1 (PEM) from letsencrypt.org/certificates
extern const char ISRG_ROOT_X1[];

WiFiClientSecure net;
PubSubClient mqtt(net);

void setup() {
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(250);
  net.setCACert(ISRG_ROOT_X1);
  mqtt.setServer("mqtt.iotaps.com", 8883);
  mqtt.setBufferSize(512);
}

void loop() {
  if (!mqtt.connected()) mqtt.connect(DEVICE_ID, CREDENTIAL_ID, SECRET);
  char id[17], payload[192];
  // Random IDs survive reboots; a counter restarting at 0 would be deduplicated.
  snprintf(id, sizeof id, "%08lx%08lx",
           (unsigned long) esp_random(), (unsigned long) esp_random());
  snprintf(payload, sizeof payload,
           "{\"schema_version\":1,\"message_id\":\"%s\","
           "\"metrics\":{\"temperature_c\":%.2f}}",
           id, readTemperature());
  mqtt.publish(TOPIC, payload);
  mqtt.loop();
  delay(10000);
}

PubSubClient speaks MQTT 3.1.1 and publishes with QoS 0. For QoS 1 on an ESP32, use the ESP-IDF esp-mqtt client.

MQTT contract (v1)

The contract is versioned in the topic (v1) and in the payload (schema_version). A breaking change gets a new version, and v1 stays supported.

Connection

SettingValue
Hostmqtt.iotaps.com:8883
TransportTLS 1.2+, publicly trusted certificate
ProtocolMQTT 5 (recommended) or 3.1.1
Client IDThe device ID
UsernameThe credential ID
PasswordThe credential secret
Topicv1/{organization_id}/{project_id}/{device_id}/telemetry
QoS1 recommended, 0 supported
KeepaliveUp to 300 seconds

Envelope

FieldTypeRules
schema_versionrequiredintegerMust be 1
message_idrequiredstring1–128 characters: letters, digits, . _ : -. Unique per device; reuse only for retries.
metricsrequiredobject1–64 entries. Names start with a letter or underscore, up to 64 characters. Values are finite numbers or booleans.
sent_atstringRFC 3339 with an offset, e.g. 2026-10-05T10:15:30Z
sequenceinteger0 to 263−1
attributesobjectUp to 32 entries: strings up to 256 characters, numbers, or booleans

Limits

A message that exceeds a limit is rejected with a reason code. Nothing is truncated or coerced.

Payload size
64 KiB per message
Metrics per message
64
Attributes per message
32
Integer range
±(253−1)
Deduplication window
7 days
History retention
30 days during the beta
Raw history query
24 hours, up to 10,000 points
Aggregated query
30 days
Active credentials
5 per device

Rejection codes

Rejected messages are listed on the device page with the code, a detail message, and the start of the payload.

CodeMeaning
payload_too_largeThe payload is larger than 64 KiB.
invalid_encodingThe payload is not UTF-8.
invalid_jsonNot valid JSON, or contains NaN or Infinity.
duplicate_keyA JSON object repeats a key.
invalid_envelopeNot a JSON object, or schema_version is missing.
unknown_fieldA top-level field is not part of the contract. Put custom data in attributes.
unsupported_schema_versionschema_version is not 1.
invalid_message_idmessage_id is missing or uses unsupported characters.
invalid_sent_atsent_at is not an RFC 3339 timestamp with an offset.
invalid_sequencesequence is not an integer from 0 to 263−1.
invalid_metricsMissing or empty, too many entries, an invalid name, or a value that is not a finite number or boolean.
invalid_attributesToo many entries, an invalid name, or an unsupported value.

Delivery guarantees

PUBACK is the broker's receipt
With QoS 1, a PUBACK means the broker accepted the message. Platform acceptance comes next, when the message is written to the durable stream.
At least once in, once in storage
Retries and redeliveries happen. A retry that reuses the same message_id is stored once within the 7-day window.
Ordering is best effort
Device time (sent_at) and receive time are both kept. Latest state follows receive time, so a late retry never overwrites a newer value.

MQTT 3.1.1 has no negative acknowledgement, so a publish to a topic the device may not use is dropped without an error. MQTT 5 clients get reason code 135 (Not authorized).

REST API

The dashboard uses the same API you do: organizations, devices, credentials, latest state, history, messages, rejections, usage, and the audit log. Requests are authenticated with the browser session cookie today; API keys for server-to-server access are planned.

Open the API reference

Troubleshooting

The broker says “not authorized” when connecting

Check that the client ID is exactly the device ID, the username is the credential ID (not the device ID), and the secret was copied in full. Revoked credentials and disabled devices are refused.

The device connects but no data appears

Publish to the device's own topic, character for character. Then look at “Rejected messages” on the device page; a reason code there means the envelope needs a fix. MQTT 5 clients get reason code 135 for a topic they may not use.

The TLS handshake fails

Check that the device clock is right, the device trusts ISRG Root X1, the client sends mqtt.iotaps.com as the server name (SNI), and it supports TLS 1.2 or newer on port 8883.

Some messages are missing after a reboot

Messages that reuse a message_id within seven days are treated as retries. Use random or UUID-based IDs rather than a counter that restarts at zero.

Still stuck? Email us with the device ID and the time you tried.