Appearance
Publishing metrics over MQTT
A connected device (see Connecting a device) records measurements by publishing JSON to one topic:
text
s/metricsThis is a relative topic: the broker places it in the device's own namespace, so never add the device ID. s/metrics is the only topic the platform records data from. Messages on other topics are accepted by the broker but not stored; they can still start a workflow (see Automating on device data).
Payload
json
{
"points": [
{
"name": "temperature",
"value": 22.5,
"kind": "GAUGE",
"unit": "CELSIUS",
"displayName": "Oven temperature",
"tags": { "zone": "front" },
"timestamp": "2026-09-24T10:15:00Z"
},
{
"name": "door_open",
"value": 0,
"kind": "STATUS",
"unit": "BOOLEAN"
}
]
}| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Identifies the metric on this device. Used as sent (no case folding). |
value | number | yes | Always a number. Send booleans as 0 / 1. |
kind | string | no | MetricKind. Default GAUGE. Only used when the metric is created. |
unit | string | no | MetricUnit. Default NONE. Only used when the metric is created. |
customUnit | string | no | Your own unit label, for use with "unit": "CUSTOM" (for example "Sheets"). Only used when the metric is created. |
displayName | string | no | Human-readable label. Only used when the metric is created. |
tags | object | no | String keys and string values, stored with this data point. |
timestamp | string | no | ISO 8601 date-time with a time zone, with or without fractional seconds (2026-09-24T10:15:00Z, 2026-09-24T12:15:00.250+02:00). Defaults to the time the platform processed the message. |
One message can carry up to 10 000 points, for one or many metrics.
Metric kinds
| Kind | Use it for |
|---|---|
GAUGE | A value that goes up and down: temperature, speed, ink level. |
COUNTER | A cumulative total that only increases: total impressions, error count. Send the current total, not the increment. |
STATUS | A discrete state as a number, such as 0/1 for a door closed/open. |
RATE | A rate of change you have computed: pages per minute, waste per hour. |
Units
Units are UPPERCASE:
| Group | Values |
|---|---|
| Dimensionless | NONE, PERCENT, RATIO, BOOLEAN |
| Count | COUNT, PIECES |
| Length | MILLIMETERS, METERS, INCHES, FEET |
| Speed | METERS_PER_MINUTE, FEET_PER_MINUTE, UNITS_PER_MINUTE, UNITS_PER_HOUR |
| Temperature | CELSIUS, FAHRENHEIT, KELVIN |
| Pressure | BAR, PSI, KILOPASCAL |
| Weight | GRAMS, KILOGRAMS, POUNDS |
| Volume | MILLILITERS, LITERS |
| Area | SQUARE_METERS, SQUARE_FEET |
| Time | MILLISECONDS, SECONDS, MINUTES, HOURS |
| Electrical | VOLTS, AMPS, WATTS, KILOWATT_HOURS |
| Data | BYTES, KILOBYTES, MEGABYTES |
| Other | CUSTOM together with customUnit |
The first point creates the metric
The first time a device sends a new name, CoCoCo creates the metric using that point's kind, unit, customUnit and displayName. After that, those fields are ignored on every later message. If the first message had the wrong kind or unit, correct the metric with the updateMetric mutation; changing what the device sends will not fix it.
When a message is dropped
The device gets no error back. The whole message is discarded when:
- it is not valid JSON, or has no
pointsarray; - any point has a missing
nameorvalue, a non-numericvalue, a lowercase or unknownkindorunit, or a non-string tag value; pointsis empty or has more than 10 000 entries;- the client ID is not a valid, unrevoked, unexpired device token ID (
dtk_…).
A timestamp that cannot be parsed does not drop the point: it is recorded at the time the message arrived.
Checking that data arrived
Open IoT → Metrics in the console, or list the device's metrics:
graphql
query DeviceMetrics($deviceId: ID!) {
listMetrics(first: 50, filter: { deviceId: { eq: $deviceId } }) {
totalCount
edges {
node {
id
name
kind
unit
customUnit
createdAt
}
}
}
}Use getMetricPoints with a metric's id to read its data points.