For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

Field
Type
Required
Description

measurements

string[]

Yes

One or more measurements. No duplicates.

items

string[]

Yes

IDs of the entities to include (1–500). See Items.

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.

fields

string[]

No

Return only these fields (up to 50). See Fields.

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: data is keyed by the entity ID of each measurement: spot_id, room_id, floor_id, or, for dt_temperature and dt_heartbeat, the sensor's device_id.

  • Ordering: each array is sorted by start, oldest first.

  • Timestamps: start and stop are UTC, in RFC 3339 format. stop is start + interval; for events it equals start.

  • 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 no data key. Treat a missing data as "no results".

On failure you get an errors array instead; see Errors.

Measurements

Measurement
Data keyed by
Intervals
Page

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

touch_events

room_id

none (events)

cleaning_alert_events

room_id

none (events)

dt_heartbeat

device_id

15m only

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: interval applies to every windowed measurement, and the whole request fails with -5009 if any of them doesn't support it. For example, spot_desk_occupancy and room_temperature can only be combined at 15m, 30m, 1h, or 1d. dt_heartbeat supports only 15m.

  • Different keys: results for measurements keyed differently (for example spot_desk_occupancy by spot_id, room_temperature by room_id) stay separate.

  • Same key: measurements keyed the same way are merged into one array per entity. room_desk_occupancy and room_motion_occupancy both return occupancy_* fields, so their points cannot be told apart. With fill_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. 1m is for recent, short windows. 1h and 1d reach 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