API
Sending readings
Uploading readings in batches of up to 1000, reading the answer, what to do with each rejection, and what happens to unknown sensor codes.
With POST /device/readings you upload readings, up to 1000 per batch. The answer tells you per reading what the server did with it, so you know exactly what you can remove from your buffer.
A batch
{
"batch_uuid": "0d4f6c1e-8a51-4c43-9b6b-3c1f0f7a2e11",
"readings": [
{"sensor": "T1", "value": 61.4, "measured_at": "2026-10-08T11:59:00Z"},
{"sensor": "R2", "value": 10, "measured_at": "2026-10-08T11:59:00Z"},
{"sensor": "T3", "value": null, "quality": "no_response", "measured_at": "2026-10-08T11:59:00Z"}
]
}
| Field | Required | Rule |
|---|---|---|
readings |
yes | 1 to 1000 readings, as a list |
batch_uuid |
no | A uuid. Keep it the same when you resend the same batch |
clock |
no | Clock anchor, only for clock correction: see Buffering and the clock |
Per reading:
| Field | Required | Rule |
|---|---|---|
sensor |
yes | A code from the device's configuration, such as T1. 1 to 16 characters: letters, digits, _, . and -. Case-sensitive |
value |
yes, with ok |
A number. May be null or left out when quality is not ok |
quality |
no | ok (default), sensor_fault, out_of_range or no_response |
measured_at |
yes | When the value was read, ISO 8601 with a time zone |
clock_synced, boot_id, mono_ns |
no | Only for clock correction |
Sending twice does no harm
A reading is unique per device, sensor and measured_at, to the second. Send it again and it counts under duplicates; the first value stays, even if the second one differs. Resending a batch after a timeout is therefore safe.
You may send batch_uuid and an Idempotency-Key header, but the server recognises duplicates by the readings themselves.
The answer
A batch that is valid as a whole gets 200, also when individual readings were rejected:
{
"accepted": 2,
"duplicates": 0,
"unmapped": ["T9"],
"rejected": [{"index": 3, "reason": "measured_at_in_future", "retry": false}]
}
| Field | Meaning |
|---|---|
accepted |
Readings newly stored |
duplicates |
Readings the server already had, including duplicates within this batch |
unmapped |
Sensor codes that are not part of the device's circuit. Those readings are in quarantine and do not count towards accepted |
rejected |
Per rejected reading: index (position in readings, from 0), reason and retry |
Then remove the whole batch from your buffer, except the rejected readings with retry: true. Those go in a later batch.
422 means the batch is not valid as a whole: no or an empty readings, more than 1000 readings, or an invalid batch_uuid or clock. The answer has a message and the errors per field. Sending it again won't help: set the batch aside and look for the bug.
Rejections per reading
reason |
Meaning | What to do |
|---|---|---|
invalid_item |
The reading is not an object, or clock_synced is not true or false |
A bug in your software: fix it |
invalid_sensor |
sensor is missing or does not match the pattern |
Fix it |
invalid_value |
value is not a number, or its absolute value is above 10⁹ |
Fix it |
value_required |
quality is ok (or missing) but there is no value |
No value? Send no_response or sensor_fault |
invalid_quality |
Unknown value for quality |
Use one of the four values |
invalid_measured_at |
Not a valid ISO 8601 timestamp with a time zone, or a date that does not exist | Fix the format |
measured_at_in_future |
More than 5 minutes ahead of the server's clock | Your device's clock runs fast: check NTP |
measured_at_too_old |
Older than 365 days | Nothing: this reading can no longer be stored |
clock_unsynced |
Taken with an unsynchronised clock, and it cannot be corrected | With retry: true, keep it and send it later. See Buffering and the clock |
too_many_unknown_sensors |
More than 16 different unknown codes in this batch | Check your sensor codes |
unmapped_quota_exceeded |
25,000 readings already quarantined today (UTC) | Check your sensor codes |
batch_spans_too_many_days |
The reading falls on an eighth calendar day (UTC) in this batch | Keep it (retry: true) and send it in a later batch |
Only batch_spans_too_many_days, and clock_unsynced without a synchronised clock anchor, come with retry: true. All other rejections are final.
Unknown sensors
A code that is not part of the circuit goes into quarantine for 90 days, so a wiring or configuration mistake can be traced.
- You won't see those readings in your charts or anywhere in the portal, and you cannot link them to a sensor afterwards.
- On the device page, under API connection errors, a line with the number of rejected readings and the unknown codes appears at most once an hour.
- Codes are case-sensitive:
t1is notT1. - Without a circuit, every code is unknown. Wait until the configuration has sensors before you upload: see Fetching the configuration.
Quality
quality |
When | What happens |
|---|---|---|
ok |
A normal reading | Counts in charts, averages, minima and maxima |
sensor_fault |
The sensor or controller reports a fault | Stored, does not count. If it persists, the Sensor fault alert follows after ten minutes |
out_of_range |
Read fine, but impossible | Send the value. Stored, does not count |
no_response |
The sensor or controller did not answer | Value null. Stored, does not count |
If a sensor sends no ok reading for a while (half an hour by default), the Sensor sends no reading alert follows. You can switch both alerts off per device under Alerts.
Timestamps and values
measured_atmay be at most 5 minutes ahead and 365 days behind. The server truncates to whole seconds and stores in UTC.- One batch falls on at most 7 calendar days (UTC). If you send oldest first, a batch covers a day or two at most.
- Send numbers, no greater than 10⁹ in absolute terms. A number as text (
"61.4") is accepted too; other text,trueorfalsegivesinvalid_value. - Send outputs such as pumps and valves as a number, the way the controller reports them: see Building your own BoilerBuddy.
All fields and types are in the API reference.