Examples
Example: curl
Trying every endpoint of the device API with curl, with real requests and answers, and fetching the configuration only when it has changed.
With curl you can try every endpoint before you write any code, and see exactly what the server answers. The examples assume circuit 201: T1 collector, T2 tank bottom, T3 tank top, R2 pump.
Setup
API=https://boilerbuddy.eu/api/v1
TOKEN=bb_live_42_... # the API key of your device, from the portal
No key yet? Pair below, or create one in the portal: see Building your own BoilerBuddy. With read -rs TOKEN the key stays out of your shell history.
Pairing
Generate a uuid (version 4) once and ask for a code:
UUID=$(python3 -c 'import uuid; print(uuid.uuid4())')
curl -s -X POST "$API/device/hello" \
-H "Content-Type: application/json" \
-d "{\"device_uuid\": \"$UUID\", \"firmware_version\": \"curl-test\"}"
As long as nobody has paired the device:
{"claimed":false,"pairing_code":"BB-7KQ4-M2XD","expires_at":"2026-10-16T09:30:00+00:00"}
Enter the code with Pair with code on the device page, then send the same command again:
{"claimed":true,"token":"bb_live_42_Xk3..."}
Store the token and use it straight away, for example below. After that, hello only returns:
{"claimed":true}
Why: see Pairing with hello.
Fetching the configuration
curl -s -i "$API/device/config" -H "Authorization: Bearer $TOKEN"
HTTP/2 200
content-type: application/json
etag: "9f2c1a0b7d3e4f56"
{"config_version":"9f2c1a0b7d3e4f56","device":{"id":42,"name":"Attic","mode":"live"},"intervals":{"scan_seconds":60},"controller":{"brand":"OEG","model":"KSW-E, KSW, KS2W","protocol":"modbus_ascii","connection":{"slave_address":128,"baudrate":9600,"parity":"E","bytesize":7,"stopbits":1,"timeout_ms":1000}},"sensors":[{"sensor":"T1","name":"Temperatuur zonnepaneel","type":"collector_temp","category":"temperature","unit":"\u00b0C","position":null,"address":{"register":38,"decimals":1,"signed":true,"function_code":3},"plausible_min":-50.0,"plausible_max":250.0}, ...]}
Just the sensor codes, with jq:
curl -s "$API/device/config" -H "Authorization: Bearer $TOKEN" | jq -r '.sensors[].sensor'
T1
T2
T3
R2
Only fetch it when something changed
Send the ETag back in If-None-Match. Nothing changed? Then you get 304 without a body:
curl -s -i "$API/device/config" \
-H "Authorization: Bearer $TOKEN" \
-H 'If-None-Match: "9f2c1a0b7d3e4f56"'
HTTP/2 304
etag: "9f2c1a0b7d3e4f56"
In a script, only replace the configuration and the ETag on a 200:
# First time: keep the body and the ETag
curl -s -D headers.txt -o config.json "$API/device/config" -H "Authorization: Bearer $TOKEN"
ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# Later: only download when something changed
STATUS=$(curl -s -D headers.txt -o new.json -w '%{http_code}' "$API/device/config" \
-H "Authorization: Bearer $TOKEN" -H "If-None-Match: $ETAG")
if [ "$STATUS" = 200 ]; then
mv new.json config.json
ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\r')
fi
Sending readings
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
BATCH=$(python3 -c 'import uuid; print(uuid.uuid4())')
curl -s -X POST "$API/device/readings" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @- <<EOF
{
"batch_uuid": "$BATCH",
"readings": [
{"sensor": "T1", "value": 61.4, "measured_at": "$NOW"},
{"sensor": "T2", "value": 38.0, "measured_at": "$NOW"},
{"sensor": "R2", "value": 10, "measured_at": "$NOW"}
]
}
EOF
{"accepted":3,"duplicates":0,"unmapped":[],"rejected":[]}
Exactly the same again (same $NOW) stores nothing twice:
{"accepted":0,"duplicates":3,"unmapped":[],"rejected":[]}
A batch with errors
Errors in individual readings still get 200. The answer says per reading what is wrong:
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -s -X POST "$API/device/readings" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @- <<EOF
{
"readings": [
{"sensor": "T1", "value": 61.5, "measured_at": "$NOW"},
{"sensor": "T9", "value": 30.0, "measured_at": "$NOW"},
{"sensor": "T2", "value": "warm", "measured_at": "$NOW"},
{"sensor": "T3", "value": 55.0, "measured_at": "2030-01-01T00:00:00Z"}
]
}
EOF
{"accepted":1,"duplicates":0,"unmapped":["T9"],"rejected":[{"index":2,"reason":"invalid_value","retry":false},{"index":3,"reason":"measured_at_in_future","retry":false}]}
T9 is not part of circuit 201 and is now in quarantine. All reasons are listed in Sending readings.
A batch that is invalid as a whole
curl -s -X POST "$API/device/readings" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"readings": []}'
{"message":"The readings field is required.","errors":{"readings":["The readings field is required."]}}
That is a 422: sending it again won't help.
Sending a status
curl -s -i -X POST "$API/device/status" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"firmware_version": "curl-test", "uptime_s": 3600, "buffered": 0}'
HTTP/2 204
The device page then shows curl-test under Firmware, and Last seen is updated.
Errors
A wrong or replaced token:
curl -s -i "$API/device/config" -H "Authorization: Bearer bb_live_42_wrong"
HTTP/2 401
www-authenticate: Bearer
content-type: application/json
{"message":"Unauthenticated."}
Too many uploads in a minute:
HTTP/2 429
retry-after: 37
content-type: application/json
{"message":"Too Many Attempts."}
Wait the number of seconds in retry-after and send the same batch again. More in Limits and error codes.