Partner MQTT Integration Guide
Connect partner IoT devices to CPI using JWT-authenticated MQTT (TOKEN auth method). Covers registration, provisioning, telemetry, commands, and HMAC signing.
Protocol version: 1.0
This guide is for IoT partners who want to connect their devices to the CPI platform using JWT-authenticated MQTT (the TOKEN auth method). It covers everything from device registration to publishing telemetry, receiving commands, and handling token lifecycle.
Overview
The CPI platform provides a secure MQTT broker for IoT devices to publish telemetry, report status, and receive commands in real time. The platform supports two authentication methods:
| Auth Method | Transport | Use Case |
|---|---|---|
| MTLS | Port 8883, mutual TLS with client certificates | High-security deployments where each device has a unique X.509 certificate |
| TOKEN (this guide) | Port 8885, server-side TLS + JWT authentication | Partner integrations where certificate management is impractical |
Both methods share the same security layers for message integrity:
- TLS encryption — all traffic is encrypted in transit (server-side TLS on port 8885)
- JWT identity — device identity verified via signed JWT token
- Per-device ACLs — JWT encodes exactly which topics a device can publish/subscribe to
- HMAC-SHA256 signatures — every message payload is signed with a per-device secret
- Replay protection — nonce + timestamp validation prevents message replay attacks
Prerequisites
Before you begin, ensure you have:
- An admin account on the CPI platform with API access
- API base URL:
https://api.cpi.lovinka.com - MQTT broker:
mqtts://mqtt.cpi.lovinka.com:8885 - An MQTT client library that supports TLS and username/password authentication
- A cryptographic library that supports HMAC-SHA256
Quick Start
Authenticate with the API
curl -X POST https://api.cpi.lovinka.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{"email": "your@email.com", "password": "your-password"}'Register a device
curl -X POST https://api.cpi.lovinka.com/api/v1/devices \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"name": "Partner Sensor 001",
"serialNumber": "PARTNER-SN-001",
"type": "SENSOR",
"authMethod": "TOKEN"
}'Save the returned id (a UUID).
Provision the device
curl -X POST https://api.cpi.lovinka.com/api/v1/devices/{deviceId}/provision \
-b cookies.txt \
-o provisioning-package.jsonSave this response securely. The hmacSecret and mqttToken are only returned once and are not stored on the platform in plaintext.
Connect and publish telemetry
const mqtt = require('mqtt');
const crypto = require('crypto');
const pkg = require('./provisioning-package.json');
const client = mqtt.connect(pkg.mqtt.broker, {
port: pkg.mqtt.port,
username: pkg.deviceId,
password: pkg.security.mqttToken,
protocol: 'mqtts',
rejectUnauthorized: true,
});
client.on('connect', () => {
const ts = Date.now();
const nonce = crypto.randomUUID();
const data = { temperature: 22.5, humidity: 45.2 };
const msg = `${pkg.deviceId}|${ts}|${nonce}|${JSON.stringify(data)}`;
const sig = crypto.createHmac('sha256', pkg.security.hmacSecret)
.update(msg).digest('hex');
client.publish(pkg.mqtt.topics.telemetry,
JSON.stringify({ ts, nonce, data, sig }));
console.log('Telemetry published!');
});import paho.mqtt.client as mqtt
import json, hmac, hashlib, time, uuid, ssl
with open('provisioning-package.json') as f:
pkg = json.load(f)
device_id = pkg['deviceId']
secret = pkg['security']['hmacSecret']
client = mqtt.Client(client_id=device_id, protocol=mqtt.MQTTv311)
client.username_pw_set(device_id, pkg['security']['mqttToken'])
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
def on_connect(client, userdata, flags, rc):
ts = int(time.time() * 1000)
nonce = str(uuid.uuid4())
data = {"temperature": 22.5, "humidity": 45.2}
msg = f"{device_id}|{ts}|{nonce}|{json.dumps(data, separators=(',', ':'))}"
sig = hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
client.publish(pkg['mqtt']['topics']['telemetry'],
json.dumps({"ts": ts, "nonce": nonce, "data": data, "sig": sig}))
print("Telemetry published!")
client.on_connect = on_connect
client.connect("mqtt.cpi.lovinka.com", port=8885, keepalive=60)
client.loop_forever()# MQTT requires a persistent connection — use an MQTT CLI tool instead:
# npm install -g mqtt
mqtt pub -h mqtt.cpi.lovinka.com -p 8885 \
-u "{deviceId}" -P "{mqttToken}" \
--protocol mqtts \
-t "cpi/{deviceId}/telemetry" \
-m '{"ts":1700000000000,"nonce":"uuid","data":{"temperature":22.5},"sig":"..."}'Verify in the dashboard
Open https://cpi.lovinka.com, navigate to your device, and confirm the telemetry data appears on the live chart.
Device Registration
Register a new device by sending a POST request to the devices endpoint.
Request
POST /api/v1/devices
Content-Type: application/json
Cookie: access_token=<jwt>{
"name": "Weather Station Alpha",
"serialNumber": "WS-ALPHA-001",
"type": "SENSOR",
"authMethod": "TOKEN",
"metadata": {
"firmware": "1.2.0",
"location": "Building A, Floor 3"
}
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable device name (1-100 chars) |
serialNumber | string | Yes | Unique manufacturer identifier (1-50 chars) |
type | enum | Yes | SENSOR, ACTUATOR, GATEWAY, or CONTROLLER |
authMethod | enum | No | TOKEN for JWT auth (defaults to MTLS) |
metadata | object | No | Arbitrary key-value pairs |
Device Lifecycle
PENDING → ACTIVE → SUSPENDED → ACTIVE (reactivated)
|
└→ DECOMMISSIONED (permanent, revoked)- PENDING — newly registered, awaiting provisioning
- ACTIVE — provisioned and operational
- SUSPENDED — temporarily disabled (e.g., auth failures), can be reactivated
- DECOMMISSIONED — permanently revoked, credentials destroyed
Device Provisioning
Provisioning generates the device's MQTT credentials and HMAC signing secret. This is a one-time operation — the secrets are returned only once.
POST /api/v1/devices/{deviceId}/provision
Cookie: access_token=<jwt>Requires ADMIN role.
Response (TOKEN auth)
{
"deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"authMethod": "TOKEN",
"mqtt": {
"broker": "mqtts://mqtt.cpi.lovinka.com",
"port": 8885,
"topics": {
"telemetry": "cpi/a1b2c3d4-.../telemetry",
"status": "cpi/a1b2c3d4-.../status",
"commands": "cpi/a1b2c3d4-.../commands",
"ack": "cpi/a1b2c3d4-.../ack"
}
},
"security": {
"hmacSecret": "4a7f...64-char-hex-string...b3e1",
"mqttToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenExpiresAt": "2025-01-15T11:30:00.000Z"
},
"protocol": {
"version": "1.0",
"signatureAlgorithm": "HMAC-SHA256",
"timestampToleranceMs": 300000
}
}Credential Storage
| Field | Sensitivity | Storage Recommendation |
|---|---|---|
security.hmacSecret | Secret — never transmitted again | HSM, encrypted keystore, or secure enclave |
security.mqttToken | Short-lived — expires in 1 hour | Memory or secure storage; refresh before expiry |
mqtt.topics | Configuration | Firmware config or flash storage |
deviceId | Identifier | Firmware config or flash storage |
MQTT Connection
Connection Parameters
| Parameter | Value |
|---|---|
| Host | mqtt.cpi.lovinka.com |
| Port | 8885 |
| Protocol | mqtts (MQTT over TLS) |
| TLS | Server-side TLS (no client certificates) |
| Username | Device ID (UUID from provisioning) |
| Password | MQTT JWT token |
| Clean Session | true (recommended) |
| Keep Alive | 60 seconds |
Topic Permissions (JWT ACLs)
| Direction | Topic | Permission |
|---|---|---|
| Device → Platform | cpi/{deviceId}/telemetry | Publish |
| Device → Platform | cpi/{deviceId}/status | Publish |
| Device → Platform | cpi/{deviceId}/ack | Publish |
| Platform → Device | cpi/{deviceId}/commands | Subscribe |
Connection Examples
const mqtt = require('mqtt');
const client = mqtt.connect('mqtts://mqtt.cpi.lovinka.com', {
port: 8885,
username: deviceId,
password: mqttToken,
protocol: 'mqtts',
rejectUnauthorized: true,
clean: true,
keepalive: 60,
clientId: deviceId,
reconnectPeriod: 5000,
});import paho.mqtt.client as mqtt
import ssl
client = mqtt.Client(client_id=device_id, protocol=mqtt.MQTTv311)
client.username_pw_set(username=device_id, password=mqtt_token)
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
client.connect("mqtt.cpi.lovinka.com", port=8885, keepalive=60)
client.loop_start()struct mosquitto *mosq = mosquitto_new(device_id, true, NULL);
mosquitto_username_pw_set(mosq, device_id, mqtt_token);
mosquitto_tls_set(mosq, NULL, NULL, NULL, NULL, NULL);
mosquitto_tls_opts_set(mosq, 1, "tlsv1.2", NULL);
mosquitto_connect(mosq, "mqtt.cpi.lovinka.com", 8885, 60);
mosquitto_loop_start(mosq);Message Formats
All messages are JSON-encoded. Every message includes an HMAC-SHA256 signature in the sig field. The deviceId is inferred from the MQTT topic (cpi/{deviceId}/...).
Telemetry
Direction: Device → Platform | Topic: cpi/{deviceId}/telemetry
{
"ts": 1700000000000,
"nonce": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"temperature": 22.5,
"humidity": 45.2,
"pressure": 1013.25
},
"sig": "a1b2c3d4e5f6...64-char-hex-string"
}| Field | Type | Description |
|---|---|---|
ts | integer | Millisecond epoch timestamp (Date.now()) |
nonce | string | Unique message ID (UUID v4 recommended) |
data | object | Sensor readings (string keys, numeric values) |
sig | string | HMAC-SHA256 hex signature |
Signature order: deviceId|ts|nonce|JSON.stringify(data)
Status
Direction: Device → Platform | Topic: cpi/{deviceId}/status
{
"ts": 1700000000000,
"nonce": "d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890",
"status": "ONLINE",
"sig": "b2c3d4e5f6a7...64-char-hex-string"
}Values: ONLINE, OFFLINE, MAINTENANCE, ERROR
Signature order: deviceId|ts|nonce|JSON.stringify({status}) — note the status is wrapped in an object.
Command (Received)
Direction: Platform → Device | Topic: cpi/{deviceId}/commands
{
"cmdId": "e5f6a7b8-c9d0-1234-e5f6-a7b8c9d01234",
"ts": 1700000000000,
"action": "REBOOT",
"payload": {},
"sig": "c3d4e5f6a7b8...64-char-hex-string"
}Signature order: deviceId|cmdId|ts|action|JSON.stringify(payload)
Always verify the command signature before executing any action. See the HMAC signing section below.
Command ACK (Sent)
Direction: Device → Platform | Topic: cpi/{deviceId}/ack
{
"cmdId": "e5f6a7b8-c9d0-1234-e5f6-a7b8c9d01234",
"status": "COMPLETED",
"ts": 1700000000000,
"sig": "d4e5f6a7b8c9...64-char-hex-string"
}Values: RECEIVED, IN_PROGRESS, COMPLETED, FAILED
Signature order: deviceId|cmdId|ts|status
HMAC Signing
Every message is signed with HMAC-SHA256 using the per-device hmacSecret from the provisioning package.
How It Works
- Construct the signing string by joining fields with
|(pipe) - Compute HMAC-SHA256 using the
hmacSecret - Encode as lowercase hex (64 characters)
- Include as the
sigfield in the JSON message
Signature Formats
| Message Type | Signing String |
|---|---|
| Telemetry | {deviceId}|{ts}|{nonce}|{JSON.stringify(data)} |
| Status | {deviceId}|{ts}|{nonce}|{JSON.stringify({status})} |
| Command | {deviceId}|{cmdId}|{ts}|{action}|{JSON.stringify(payload)} |
| ACK | {deviceId}|{cmdId}|{ts}|{status} |
JSON.stringify notes: Use compact JSON with no extra whitespace. In Python, use json.dumps(data, separators=(',', ':')). In C, ensure no whitespace between keys, colons, values, or commas.
Verifying Incoming Commands
function verifyCommandSignature(deviceId, cmd, secret) {
const msg = `${deviceId}|${cmd.cmdId}|${cmd.ts}|${cmd.action}|${JSON.stringify(cmd.payload)}`;
const expected = crypto.createHmac('sha256', secret).update(msg).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(cmd.sig, 'hex')
);
}def verify_command(device_id: str, cmd: dict, secret: str) -> bool:
msg = f"{device_id}|{cmd['cmdId']}|{cmd['ts']}|{cmd['action']}|{json.dumps(cmd['payload'], separators=(',', ':'))}"
expected = hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, cmd['sig'])int verify_command(const char *device_id, const char *cmd_id,
long long ts, const char *action,
const char *payload_json, const char *sig,
const char *secret) {
char message[4096], expected[65];
snprintf(message, sizeof(message), "%s|%s|%lld|%s|%s",
device_id, cmd_id, ts, action, payload_json);
hmac_sign(secret, message, expected, 65);
return CRYPTO_memcmp(expected, sig, 64) == 0;
}Timestamp and Nonce Validation
- Timestamp tolerance: ±5 minutes (300,000 ms) from current time
- Nonce uniqueness: Duplicate nonces within a 5-minute window are rejected
- Recommendation: Use UUID v4 (
crypto.randomUUID()in Node.js,uuid.uuid4()in Python)
Signing Code Examples
const crypto = require('crypto');
function signTelemetry(deviceId, ts, nonce, data, secret) {
const msg = `${deviceId}|${ts}|${nonce}|${JSON.stringify(data)}`;
return crypto.createHmac('sha256', secret).update(msg).digest('hex');
}
function signAck(deviceId, cmdId, ts, status, secret) {
const msg = `${deviceId}|${cmdId}|${ts}|${status}`;
return crypto.createHmac('sha256', secret).update(msg).digest('hex');
}
function signStatus(deviceId, ts, nonce, status, secret) {
const msg = `${deviceId}|${ts}|${nonce}|${JSON.stringify({ status })}`;
return crypto.createHmac('sha256', secret).update(msg).digest('hex');
}import hmac, hashlib, json
def sign_telemetry(device_id, ts, nonce, data, secret):
msg = f"{device_id}|{ts}|{nonce}|{json.dumps(data, separators=(',', ':'))}"
return hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
def sign_ack(device_id, cmd_id, ts, status, secret):
msg = f"{device_id}|{cmd_id}|{ts}|{status}"
return hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
def sign_status(device_id, ts, nonce, status, secret):
status_json = json.dumps({"status": status}, separators=(',', ':'))
msg = f"{device_id}|{ts}|{nonce}|{status_json}"
return hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()#include <openssl/hmac.h>
#include <stdio.h>
#include <string.h>
int hmac_sign(const char *secret, const char *message,
char *out, size_t out_len) {
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digest_len = 0;
HMAC(EVP_sha256(), secret, (int)strlen(secret),
(const unsigned char *)message, strlen(message),
digest, &digest_len);
if (out_len < digest_len * 2 + 1) return -1;
for (unsigned int i = 0; i < digest_len; i++)
snprintf(out + i * 2, 3, "%02x", digest[i]);
out[digest_len * 2] = '\0';
return 0;
}
int sign_telemetry(const char *device_id, long long ts,
const char *nonce, const char *data_json,
const char *secret, char *sig_out) {
char message[4096];
snprintf(message, sizeof(message), "%s|%lld|%s|%s",
device_id, ts, nonce, data_json);
return hmac_sign(secret, message, sig_out, 65);
}
int sign_ack(const char *device_id, const char *cmd_id,
long long ts, const char *status,
const char *secret, char *sig_out) {
char message[1024];
snprintf(message, sizeof(message), "%s|%s|%lld|%s",
device_id, cmd_id, ts, status);
return hmac_sign(secret, message, sig_out, 65);
}
int sign_status(const char *device_id, long long ts,
const char *nonce, const char *status,
const char *secret, char *sig_out) {
char message[4096];
snprintf(message, sizeof(message),
"%s|%lld|%s|{\"status\":\"%s\"}",
device_id, ts, nonce, status);
return hmac_sign(secret, message, sig_out, 65);
}Token Refresh
MQTT JWT tokens are short-lived by design:
| Parameter | Value |
|---|---|
| Default expiry | 1 hour (3,600 seconds) |
| Maximum expiry | 24 hours (86,400 seconds) |
Refresh Endpoint
POST /api/v1/devices/{deviceId}/mqtt-token
Cookie: access_token=<admin-jwt>Requires ADMIN role.
Recommended Strategy
Since the device itself cannot call the REST API, token refresh involves a backend-to-backend flow:
Partner Cloud CPI API MQTT Broker
| | |
|-- POST /devices/:id/token --> | |
|<-- { token, expiresAt } ----- | |
| |
|-- Push new token to device (out-of-band) ------> |
| Device reconnectsRecommendations:
- Refresh at 80% of token lifetime (48 minutes for 1-hour tokens)
- Handle disconnects gracefully — trigger immediate refresh
- Retry with exponential backoff (1s, 2s, 4s, 8s, max 60s)
- Deliver new tokens via your management channel (BLE, cellular, etc.)
Error Handling
MQTT Connection Errors
| Error | Cause | Resolution |
|---|---|---|
| Connection refused (5) | Invalid/expired JWT token | Refresh the token via API |
| Connection refused (4) | Malformed username/password | Verify username = deviceId, password = JWT |
| TLS handshake failure | TLS config issue | Ensure TLS 1.2+ and rejectUnauthorized: true |
| Unexpected disconnect | Token expired mid-session | Implement automatic token refresh |
HMAC Validation Errors
The platform silently drops messages that fail HMAC validation:
| Symptom | Cause | Resolution |
|---|---|---|
| Telemetry not appearing | Invalid signature | Verify signing string format exactly |
| Telemetry not appearing | Timestamp out of tolerance | Sync device clock (NTP) |
| Telemetry not appearing | Duplicate nonce | Generate fresh UUID per message |
| Telemetry not appearing | JSON serialization mismatch | Use compact JSON: {"key":value} |
Device Suspension
10 auth failures in 5 minutes triggers automatic suspension. The device must be manually reactivated by an admin via POST /api/v1/devices/{id}/reactivate.
Security Best Practices
- Never hardcode the
hmacSecretin source code — use HSM, secure enclave, or encrypted storage - Never log JWT tokens in plaintext
- Always verify incoming command signatures before executing
- Always use TLS (
mqtts://on port 8885) — never plain MQTT - Keep device clock synced via NTP (±5 minute tolerance)
- Rotate secrets if compromise is suspected via
POST /devices/:id/rotate-secret(60-minute grace period)
Complete Example: Node.js Simulator
#!/usr/bin/env node
const mqtt = require('mqtt');
const crypto = require('crypto');
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8'));
const { deviceId } = pkg;
const { hmacSecret, mqttToken } = pkg.security;
const topics = pkg.mqtt.topics;
function signTelemetry(ts, nonce, data) {
const msg = `${deviceId}|${ts}|${nonce}|${JSON.stringify(data)}`;
return crypto.createHmac('sha256', hmacSecret).update(msg).digest('hex');
}
function signStatus(ts, nonce, status) {
const msg = `${deviceId}|${ts}|${nonce}|${JSON.stringify({ status })}`;
return crypto.createHmac('sha256', hmacSecret).update(msg).digest('hex');
}
function signAck(cmdId, ts, status) {
const msg = `${deviceId}|${cmdId}|${ts}|${status}`;
return crypto.createHmac('sha256', hmacSecret).update(msg).digest('hex');
}
const client = mqtt.connect(pkg.mqtt.broker, {
port: pkg.mqtt.port,
username: deviceId,
password: mqttToken,
protocol: 'mqtts',
rejectUnauthorized: true,
clean: true,
keepalive: 60,
clientId: deviceId,
reconnectPeriod: 5000,
});
client.on('connect', () => {
console.log('Connected to CPI MQTT broker');
const ts = Date.now();
const nonce = crypto.randomUUID();
client.publish(topics.status,
JSON.stringify({ ts, nonce, status: 'ONLINE', sig: signStatus(ts, nonce, 'ONLINE') }));
client.subscribe(topics.commands);
});
client.on('message', (topic, payload) => {
if (topic !== topics.commands) return;
const cmd = JSON.parse(payload.toString());
console.log(`Command: ${cmd.action} (${cmd.cmdId})`);
const ackTs = Date.now();
client.publish(topics.ack, JSON.stringify({
cmdId: cmd.cmdId, status: 'RECEIVED', ts: ackTs,
sig: signAck(cmd.cmdId, ackTs, 'RECEIVED'),
}));
setTimeout(() => {
const doneTs = Date.now();
client.publish(topics.ack, JSON.stringify({
cmdId: cmd.cmdId, status: 'COMPLETED', ts: doneTs,
sig: signAck(cmd.cmdId, doneTs, 'COMPLETED'),
}));
}, 2000);
});
setInterval(() => {
const ts = Date.now();
const nonce = crypto.randomUUID();
const data = {
temperature: Math.round((20 + Math.random() * 10) * 100) / 100,
humidity: Math.round((40 + Math.random() * 20) * 100) / 100,
};
client.publish(topics.telemetry,
JSON.stringify({ ts, nonce, data, sig: signTelemetry(ts, nonce, data) }));
}, 10_000);
process.on('SIGINT', () => {
const ts = Date.now();
const nonce = crypto.randomUUID();
client.publish(topics.status,
JSON.stringify({ ts, nonce, status: 'OFFLINE', sig: signStatus(ts, nonce, 'OFFLINE') }),
{}, () => client.end(false, () => process.exit(0)));
});Run with: node device-simulator.js ./provisioning-package.json
API Reference Summary
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/v1/auth/login | POST | Public | Authenticate |
/api/v1/devices | POST | Cookie | Register a device |
/api/v1/devices/:id | GET | Cookie | Get device details |
/api/v1/devices/:id/provision | POST | ADMIN | Generate provisioning package |
/api/v1/devices/:id/mqtt-token | POST | ADMIN | Issue fresh MQTT JWT |
/api/v1/devices/:id/rotate-secret | POST | ADMIN | Rotate HMAC secret |
/api/v1/devices/:id/suspend | POST | ADMIN | Suspend a device |
/api/v1/devices/:id/reactivate | POST | ADMIN | Reactivate a device |
/api/v1/devices/:id/revoke | POST | ADMIN | Decommission permanently |
Troubleshooting Checklist
- Is the device in
ACTIVElifecycle status? - Is the MQTT token still valid?
- Is the device clock synchronized (±5 minutes)?
- Are you connecting to port 8885 (TOKEN auth)?
- Is
usernameset to the device UUID (not serial number)? - Is
passwordset to the JWT token (not HMAC secret)? - Are you using
mqtts://protocol? - Is
JSON.stringifyproducing compact JSON? - Is each nonce unique (UUID v4)?
- Is
deviceIdin the signing string but not in the message payload?