Skip to content
BoilerBuddy

API

API reference

Every endpoint, field and response of the device API, straight from the contract (OpenAPI 3.1).

Base URL
https://boilerbuddy.eu/api/v1
Contract
BoilerBuddy device API 1.0.0

Introduction

The API a BoilerBuddy device — our own BoilerBuddy Alpo, or something you build yourself — uses to fetch its configuration and upload readings.

Ownership. This file is the contract: the server implements it, and the device software follows it. A change to the API is made here first.

Time. All timestamps are ISO-8601 with a timezone. The server stores UTC.

Idempotency. A reading is identified by device, sensor and measured_at (to the second). Uploading the same reading twice is harmless: the first one wins and the second counts as a duplicate. The Idempotency-Key header is accepted but the server does not depend on it.

Partial acceptance. A well-formed batch always gets 200, also when some readings are rejected, duplicates or for unknown sensors. The device may then delete the batch from its buffer, except the rejected readings marked retry: true: it keeps those and sends them again in a later batch. 422 means the batch as a whole is malformed and resending it unchanged will not help.

Limits. Per token: 60 requests per minute in total, of which at most 10 uploads to /device/readings. Per IP address: 600 requests per minute. A 429 carries Retry-After; wait that long and resend the same batch. Catching up on a buffer therefore goes at up to 10 000 readings per minute. Within a batch, readings may fall on at most 7 calendar days (UTC): send the buffer in order, oldest first, and a batch stays within a day or two. A device may quarantine at most 25 000 readings for unknown sensors per day.

Authentication

Authorization: Bearer …

bb_live_<device id>_<secret> for a live device, bb_test_… for a simulated one. Shown once when the device is created or the token replaced.

GET/device/config

What to read, and how

With a token (Authorization: Bearer)

Sensors of the device's circuit with where each lives on the controller, the controller's protocol and connection settings, and the scan interval. Poll with If-None-Match to get 304 when nothing changed.

Parameters

FieldInDescription
If-None-Match
string
header

Responses

  • 200

    Current configuration

    Schema: DeviceConfig

    Headers: ETag

  • 304

    Unchanged since the given version

  • 401

    Missing, unknown, revoked or replaced token, or an inactive device

    Schema: Error

  • 429

    More than 60 requests per minute for this token, more than 10 uploads of readings per minute, or more than 600 requests per minute from this address. Respect Retry-After.

    Headers: Retry-After

POST/device/readings

Upload a batch of readings

With a token (Authorization: Bearer)

Parameters

FieldInDescription
Idempotency-Key
string (uuid)
header

Optional; the batch_uuid. Correctness does not depend on it.

Request body

Schema: ReadingsBatch

{
    "batch_uuid": "0d4f6c1e-8a51-4c43-9b6b-3c1f0f7a2e11",
    "readings": [
        {
            "sensor": "T1",
            "value": 61.4,
            "measured_at": "2026-10-08T11:59:00Z"
        },
        {
            "sensor": "R2",
            "value": 1,
            "measured_at": "2026-10-08T11:59:00Z"
        },
        {
            "sensor": "T5",
            "value": null,
            "quality": "no_response",
            "measured_at": "2026-10-08T11:59:00Z"
        }
    ]
}

Responses

  • 200

    Batch processed. Delete it from the buffer.

    Schema: ReadingsResult

  • 401

    Missing, unknown, revoked or replaced token, or an inactive device

    Schema: Error

  • 422

    The request as a whole is invalid; do not resend it unchanged

    Schema: ValidationError

  • 429

    More than 60 requests per minute for this token, more than 10 uploads of readings per minute, or more than 600 requests per minute from this address. Respect Retry-After.

    Headers: Retry-After

  • 503

    Temporarily unavailable; retry with backoff

POST/device/status

Heartbeat

With a token (Authorization: Bearer)

Send periodically, also when there is nothing to upload, so the server can tell "offline" from "quiet". Every authenticated request updates last-seen; this one also stores the device's own status.

Request body

Schema: DeviceStatus

Responses

  • 204

    Stored

  • 401

    Missing, unknown, revoked or replaced token, or an inactive device

    Schema: Error

  • 422

    The request as a whole is invalid; do not resend it unchanged

    Schema: ValidationError

  • 429

    More than 60 requests per minute for this token, more than 10 uploads of readings per minute, or more than 600 requests per minute from this address. Respect Retry-After.

    Headers: Retry-After

POST/device/hello

Pair a new device

No token

For a device without a token. It generates a uuid once, keeps it, and sends it here. Until a user claims the device, the answer is a pairing code to show (BB-XXXX-XXXX, valid 24 hours; poll every 15 seconds or so, the same code comes back until it expires, then a new one). The user enters the code in the portal. The next hello returns the token. Store it before anything else, then use it (/device/status or /device/config). Until the device has authenticated with it, every hello returns a new token and the previous one stops working, so a lost answer only means asking again. Once the device has used its token, claimed is true without a token.

If the owner makes a new key by hand in the portal, the pairing is closed: the next hello gets a new pairing code, and nothing revokes the key made by hand.

Keep the uuid secret until the device has used its token: whoever knows it collects one. Limited to 10 requests per minute per uuid, and 30 per minute per IP address (an IPv6 /64 counts as one address), so a few boxes behind one router can pair at the same time.

Request body

Schema: Hello

{
    "device_uuid": "7b0e2f4c-1d3a-4e5b-9c6d-8f7a6b5c4d3e",
    "hw_serial": "00000000abcd1234",
    "firmware_version": "0.3.0"
}

Responses

  • 200

    Pairing state

    Schema: HelloResult

    waiting

    {
        "claimed": false,
        "pairing_code": "BB-7KQ4-M2XD",
        "expires_at": "2026-10-10T12:00:00+00:00"
    }

    Claimed; the token is not used yet (repeated with a new token until it is)

    {
        "claimed": true,
        "token": "bb_live_12_aBcD…"
    }

    Claimed, and the device has used its token

    {
        "claimed": true
    }
  • 422

    The request as a whole is invalid; do not resend it unchanged

    Schema: ValidationError

  • 429

    More than 10 requests per minute for this uuid, or 30 from this address. Respect Retry-After.

    Headers: Retry-After

Schemas

ReadingsBatch

FieldDescription
batch_uuid
string (uuid)

Stable per batch, also when resent after a crash.

clock
readingsrequired

minItems: 1 · maxItems: 1000

Reading

FieldDescription
sensorrequired
string

Example: T1

pattern: ^[A-Za-z0-9_.\-]{1,16}$

value
number | null

Required when quality is ok; may be null or left out otherwise. Numeric strings are accepted for compatibility. Absolute values above 1e9 are rejected.

quality
string

Only ok readings count towards averages, minima and maxima.

Allowed values: oksensor_faultout_of_rangeno_response

Default: ok

measured_atrequired
string (date-time)

When the value was read. At most 5 minutes in the future and 365 days in the past. Stored to the second.

clock_synced
boolean

false if the wall clock was not yet synchronised when this reading was taken. The server then corrects measured_at from mono_ns and the batch's clock anchor, provided boot_id matches; otherwise the reading is rejected with clock_unsynced. Without a synchronised anchor that rejection carries retry: true: send the reading again once the clock is synchronised.

Default: true

boot_id
string

Required for clock correction.

mono_ns
integer

Monotonic clock at measurement.

ClockAnchor

The device clock at the moment of sending. Optional; only needed for clock correction.

FieldDescription
boot_idrequired
string

maxLength: 64

syncedrequired
boolean
sent_atrequired
string (date-time)
sent_mono_nsrequired
integer

minimum: 0

ReadingsResult

FieldDescription
acceptedrequired
integer

Newly stored readings

duplicatesrequired
integer

Readings the server already had

unmappedrequired
string[]

Sensor codes that are not part of the device's circuit. These readings are kept in quarantine for 90 days, so a wiring or configuration mistake can be found; they are not counted as accepted. At most 16 unknown codes per batch.

rejectedrequired
object[]
rejected[].indexrequired
integer

Position in readings

rejected[].retryrequired
boolean

Keep this reading and send it again in a later batch. Only for clock_unsynced without a synchronised anchor, and batch_spans_too_many_days.

rejected[].reasonrequired
string

Allowed values: invalid_iteminvalid_sensorinvalid_valuevalue_requiredinvalid_qualityinvalid_measured_atmeasured_at_in_futuremeasured_at_too_oldclock_unsyncedtoo_many_unknown_sensorsunmapped_quota_exceededbatch_spans_too_many_days

DeviceConfig

FieldDescription
config_versionrequired
string

Hash of the content; equals the ETag.

devicerequired
object
device.id
integer
device.name
string
device.mode
string

Allowed values: livesimulated

intervalsrequired
object
intervals.scan_seconds
integer

Example: 60

controllerrequired
object | null

null while the device has no circuit

controller.brandrequired
string

Example: OEG

controller.modelrequired
string

Example: KSW-E, KSW, KS2W

controller.protocolrequired
string

How the controller is read; now modbus_ascii or modbus_rtu. More protocols may follow, so take any string and skip a controller whose protocol you do not know.

Example: modbus_ascii

controller.connectionrequired

Settings in the shape of the protocol.

sensorsrequired

SensorConfig

FieldDescription
sensor
string

Example: T1

name
string

Example: Temperatuur zonnepaneel

type
string | null

Example: collector_temp

category
string | null

Allowed values: temperaturerelayenergyflownull

unit
string | null

Example: °C

position
integer | null

Which tank or panel

address

Where the controller keeps the value, in the shape of the controller's protocol; null when it does not have this sensor.

plausible_min
number | null
plausible_max
number | null

ModbusConnection

For modbus_ascii and modbus_rtu

FieldDescription
slave_address
integer

Example: 128

baudrate
integer

Example: 9600

parity
string

Allowed values: NEO

bytesize
integer

Example: 7

stopbits
integer

Example: 1

timeout_ms
integer

Example: 1000

ModbusAddress

For modbus_ascii and modbus_rtu. One 16-bit register, divided by 10 to the power of decimals.

FieldDescription
register
integer

Example: 38

decimals
integer

Example: 1

signed
boolean
function_code
integer

Allowed values: 34

DeviceStatus

FieldDescription
firmware_version
string

maxLength: 32

uptime_s
integer

minimum: 0

ip
string
rssi
integer

minimum: -120 · maximum: 0

buffered
integer

Readings waiting in the local buffer

minimum: 0

last_error
string

maxLength: 500

Hello

FieldDescription
device_uuidrequired
string (uuid)

Generated once by the device and kept; random (version 4).

hw_serial
string

For example the Pi's CPU serial

maxLength: 64

firmware_version
string

maxLength: 32

unit_key
string

Only a BoilerBuddy Alpo has one, from production. Claimed by the code on its sticker, it then gets its token without showing a code. Your own hardware leaves it out.

maxLength: 64

HelloResult

FieldDescription
claimedrequired
boolean
pairing_code
string

Only while unclaimed. Show it to the user.

pattern: ^BB-[2-9A-HJ-NP-Z]{4}-[2-9A-HJ-NP-Z]{4}$

expires_at
string (date-time)

Only while unclaimed

token
string

After the claim, until the device has authenticated with a token: each answer carries a new one and the previous stops working. Never sent once one was used.

Error

FieldDescription
message
string

ValidationError

FieldDescription
message
string
errors
object