Insights
OptiPeople Data Platform MQTT JSON schema
by OptiPeople Team

This is a guide for PLC programmers and integrators who want to send machine data straight to the OptiPeople Data Platform over MQTT. It covers how to connect, how a message is built, and what to send for each type of data.
The platform receives device data through Azure IoT Hub. Your PLC or gateway acts as one device on that hub and publishes JSON messages to it.
What you need from us
- IoT Hub hostname, in the form
<hub>.azure-devices.net - Device ID, for example
OM01001 - Device primary key, used to generate the password
The device must also be linked to a machine in the portal before its data shows up.
Connection settings
| Setting | Value |
|---|---|
| Broker host | <hub>.azure-devices.net |
| Port | 8883 (MQTT 3.1.1 over TLS) |
| Client ID | <deviceId> |
| Username | <hub>.azure-devices.net/<deviceId>/?api-version=2021-04-12 |
| Password | A SAS token generated from the device key |
| Publish topic | devices/<deviceId>/messages/events/$.ct=application%2Fjson&$.ce=utf-8 |
| QoS | 1 |
The server certificate chains to the Azure IoT Hub root certificates, so the PLC must trust those.
The password is a shared access signature (SAS) token, not the device key itself. It looks like this:
SharedAccessSignature sr=<hub>.azure-devices.net%2Fdevices%2F<deviceId>&sig=<signature>&se=<expiry>
The signature is an HMAC-SHA256 of the URL-encoded resource URI and the expiry time, signed with the base64-decoded device key. The token stops working at the expiry time, so pick a validity that matches how often anyone will touch the PLC. For a PLC that is set up once and left alone, a long validity (years) is common.
We have a small Windows tool, the Opti MQTT Tester, that generates every value in the table above, including the SAS token, and sends a test message so you can check the result in portal.optipeople.dk before the PLC is wired up. Ask us for it.
Message structure
Every message has the same envelope:
{
"time": "2026-09-28T10:00:00.000Z",
"inputType": "MachineState",
"functions": [
{
"deviceId": "OM01001",
"name": "state",
"value": "Runtime",
"time": "2026-09-28T10:00:00.000Z"
}
]
}
time: when the message was sent. Thetimeinside each entry infunctionsis when that event happened. Both are UTC in ISO 8601 with milliseconds, ending inZ.inputType: the kind of data. The four you use areMachineState,PartCounter,TelemetryandBatchSetting.functions: one or more entries with the actual data. Several counters or telemetry values can go in one message as separate entries.deviceId: the device ID you got from us.name: what the value is, for examplestate, a counter name or a telemetry parameter.value: the value itself. It is always a JSON string, even when it is a number.
Machine state
{
"time": "2026-09-28T10:00:00.000Z",
"inputType": "MachineState",
"functions": [
{
"deviceId": "OM01001",
"name": "state",
"value": "Runtime",
"time": "2026-09-28T10:00:00.000Z"
}
]
}
name is always state. value is one of Runtime, Downtime, Stopped or Offline. Most integrations only send Runtime and Downtime, and only when the state changes.
Instead of Downtime, you can also send a stop reason as text in value, so the stop arrives with its reason already set.
Part counters
{
"time": "2026-09-28T10:00:00.000Z",
"inputType": "PartCounter",
"functions": [
{
"deviceId": "OM01001",
"name": "counter",
"value": "10",
"time": "2026-09-28T10:00:00.000Z"
}
]
}
namemust match a counter set up for the device in the portal. Use the exact name you configured there.valueis the number of units counted since the previous message, not a running total.- A negative value registers as rejects. Send
"-1"to report one rejected unit on that counter.
Telemetry
{
"time": "2026-09-28T10:00:00.000Z",
"inputType": "Telemetry",
"functions": [
{
"deviceId": "OM01001",
"name": "vibration",
"value": "1.23",
"type": "2",
"time": "2026-09-28T10:00:00.000Z"
}
]
}
Telemetry is for process values such as temperature, humidity or vibration, and for text values such as a product code.
namemust match a telemetry parameter set up for the device in the portal.typetells the platform how to read the value:"1"for integers,"2"for decimals,"3"for text.
Starting an order or batch
{
"time": "2026-09-28T10:00:00.000Z",
"inputType": "BatchSetting",
"functions": [
{
"deviceId": "OM01001",
"name": "9025410002",
"value": "startBatch",
"e": "100",
"time": "2026-09-28T10:00:00.000Z",
"customFields": [
{ "n": "OrderNo", "v": "9649952" }
]
}
]
}
BatchSetting tells the platform that a new order, batch or unit has started on the machine. The keys are kept short to save space in the PLC.
name: the order or batch name. It must not be empty.value: alwaysstartBatch.e: optional. The expected speed in units per hour. Leave it out to keep the expected speed already set for that unit in the portal.customFields: optional. A list of{"n": name, "v": value}pairs with extra information about the order.
If your ERP or MES knows the order rather than the PLC, you can start orders through the REST API instead. See how to use the OptiPeople Data Platform API.
Rules to keep in mind
- Names must exist in the portal. A counter or telemetry name that is not set up for the device is accepted by the hub but silently dropped. You get no error.
- No more than one message every 5 seconds per device. Devices that send faster are throttled or disabled. Put several values in one message rather than sending them one by one.
- Values are strings.
"10", not10. - Timestamps are UTC. Not local PLC time.
The short version
- Connect to
<hub>.azure-devices.neton port 8883 with the device ID as client ID and a SAS token as password - Publish JSON to
devices/<deviceId>/messages/events/$.ct=application%2Fjson&$.ce=utf-8 - Use
MachineStatefor runtime and downtime,PartCounterfor produced and rejected units,Telemetryfor process values andBatchSettingto start an order - Match every name to what is set up in the portal, send values as strings, and keep to one message every 5 seconds