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.
Sign in
Open the dashboard.
Create an organization
A default project is created with it.
Add a device
Its client ID and topic are generated.
Create a credential
Copy the secret; it is shown once.
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
| Setting | Value |
|---|---|
| Host | mqtt.iotaps.com:8883 |
| Transport | TLS 1.2+, publicly trusted certificate |
| Protocol | MQTT 5 (recommended) or 3.1.1 |
| Client ID | The device ID |
| Username | The credential ID |
| Password | The credential secret |
| Topic | v1/ |
| QoS | 1 recommended, 0 supported |
| Keepalive | Up to 300 seconds |
Envelope
| Field | Type | Rules |
|---|---|---|
schema_versionrequired | integer | Must be 1 |
message_idrequired | string | 1–128 characters: letters, digits, . _ : -. Unique per device; reuse only for retries. |
metricsrequired | object | 1–64 entries. Names start with a letter or underscore, up to 64 characters. Values are finite numbers or booleans. |
sent_at | string | RFC 3339 with an offset, e.g. 2026-10-05T10:15:30Z |
sequence | integer | 0 to 263−1 |
attributes | object | Up 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.
| Code | Meaning |
|---|---|
payload_too_large | The payload is larger than 64 KiB. |
invalid_encoding | The payload is not UTF-8. |
invalid_json | Not valid JSON, or contains NaN or Infinity. |
duplicate_key | A JSON object repeats a key. |
invalid_envelope | Not a JSON object, or schema_version is missing. |
unknown_field | A top-level field is not part of the contract. Put custom data in attributes. |
unsupported_schema_version | schema_version is not 1. |
invalid_message_id | message_id is missing or uses unsupported characters. |
invalid_sent_at | sent_at is not an RFC 3339 timestamp with an offset. |
invalid_sequence | sequence is not an integer from 0 to 263−1. |
invalid_metrics | Missing or empty, too many entries, an invalid name, or a value that is not a finite number or boolean. |
invalid_attributes | Too 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_idis 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.
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.