Reporting API Overview
Query pre-aggregated time-series data for third-party sensors: occupancy, temperature, door/window activity, events, and device heartbeat.
POST https://api-integrations.butlr.io/api/v1/reporting
Content-Type: application/json
Authorization: Bearer [insert access_token here]See Authentication & Rate Limits to get a token.
Quick start
Hourly desk occupancy for two rooms over one local day:
curl -X POST https://api-integrations.butlr.io/api/v1/reporting \
-H "Authorization: Bearer [insert access_token here]" \
-H "Content-Type: application/json" \
-d '{
"measurements": ["room_desk_occupancy"],
"items": ["room_XXXX", "room_YYYY"],
"start": "2026-09-14",
"stop": "2026-09-15",
"timezone": "America/New_York",
"interval": "1h"
}'{
"data": {
"room_XXXX": [
{
"start": "2026-09-14T13:00:00Z",
"stop": "2026-09-14T14:00:00Z",
"room_id": "room_XXXX",
"room_name": "Open Office East",
"room_custom_id": "",
"floor_id": "space_XXXX",
"floor_name": "Floor 3",
"floor_custom_id": "",
"building_id": "building_XXXX",
"building_name": "HQ",
"site_id": "site_XXXX",
"site_name": "Main Campus",
"total_spots": 12,
"occupancy_min": 2,
"occupancy_max": 7,
"occupancy_mean": 4.25,
"occupancy_median": 4.25,
"occupancy_first": 3,
"occupancy_last": 5,
"occupancy_sum": 255,
"presence_time_secs_sum": 14820
}
],
"room_YYYY": [ … ]
}
}In this example, 4.25 of the room's 12 desks were occupied on average between 9:00 and 10:00 New York time. occupancy_median equals occupancy_mean because a true median is only available at 15m. See Occupancy for what each field means.
Request body
start
string
Yes
Start of the range, inclusive. RFC 3339 with an offset (2026-09-14T09:00:00-04:00, 2026-09-14T13:00:00Z) or a date (2026-09-14).
stop
string
Yes
End of the range, exclusive. Same formats as start. Must be after start. Windows are selected by their start: a window that starts before stop is returned whole, even if it ends after stop.
interval
string
Yes, unless all measurements are events
Window size: 1m, 5m, 15m, 30m, 1h, or 1d. Each measurement supports a subset, and every measurement in the request must support the one you send.
timezone
string
No
IANA time zone (e.g. America/New_York) for interpreting date-only start/stop, and for stepping days with fill_gaps at 1d. Defaults to UTC. See Time Zones & Filling Gaps.
exclude_items
string[]
No
IDs to leave out (up to 500). Same format as items.
fill_gaps
boolean
No
Add an empty data point for every window that has no data. Default false. See Filling gaps.
Fields not listed here are ignored. Send the body as JSON with Content-Type: application/json.
Response
Keys:
datais keyed by the entity ID of each measurement:spot_id,room_id,floor_id, or, fordt_temperatureanddt_heartbeat, the sensor'sdevice_id.Ordering: each array is sorted by
start, oldest first.Timestamps:
startandstopare UTC, in RFC 3339 format.stopisstart+ interval; for events it equalsstart.Missing entities: an entity with no data in the range is absent from
data. It does not appear as an empty array.No data at all: the response is an empty object,
{}, with nodatakey. Treat a missingdataas "no results".
On failure you get an errors array instead; see Errors.
Measurements
spot_desk_occupancy, room_desk_occupancy, floor_desk_occupancy
spot_id / room_id / floor_id
1m 15m 30m 1h 1d
spot_motion_occupancy, room_motion_occupancy, floor_motion_occupancy
spot_id / room_id / floor_id
1m 15m 30m 1h 1d
dt_temperature, room_temperature, floor_temperature
device_id / room_id / floor_id
5m 15m 30m 1h 1d
room_door_window_activity, floor_door_window_activity
room_id / floor_id
1m 15m 30m 1h 1d
Asking for an interval that a measurement does not support returns 400 with code -5009, and the error names the measurement. Event measurements have no intervals: any interval sent with them is ignored.
Combining measurements
You can request several measurements in one call, and they run in parallel. All results go into the same data object, keyed by entity ID, with no label saying which measurement a point came from.
One interval for all:
intervalapplies to every windowed measurement, and the whole request fails with-5009if any of them doesn't support it. For example,spot_desk_occupancyandroom_temperaturecan only be combined at15m,30m,1h, or1d.dt_heartbeatsupports only15m.Different keys: results for measurements keyed differently (for example
spot_desk_occupancybyspot_id,room_temperaturebyroom_id) stay separate.Same key: measurements keyed the same way are merged into one array per entity.
room_desk_occupancyandroom_motion_occupancyboth returnoccupancy_*fields, so their points cannot be told apart. Withfill_gaps, only one point per window survives, and which one can change between calls. Request measurements that share a key separately.
If any part of the query fails, you get an error and no partial data.
Tips
Pick the coarsest interval that answers the question.
1mis for recent, short windows.1hand1dreach furthest back (see Query Limits).Cache past data. The API does not cache: every request reads from storage. Ranges that ended more than a day ago are good candidates for client-side caching. Recent ranges can still receive late data from sensors that were briefly offline.
Stay under the rate limit by batching measurements that have different keys and share a supported interval into one request.
Last updated