> For the complete documentation index, see [llms.txt](https://docs.butlr.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.butlr.io/integrations-api-third-party-sensors/reporting-api.md).

# Reporting API Overview

Query pre-aggregated time-series data for third-party sensors: occupancy, temperature, door/window activity, events, and device heartbeat.

```http
POST https://api-integrations.butlr.io/api/v1/reporting
Content-Type: application/json
Authorization: Bearer [insert access_token here]
```

See [Authentication & Rate Limits](/integrations-api-third-party-sensors/authentication.md) to get a token.

## Quick start

Hourly desk occupancy for two rooms over one local day:

```bash
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"
  }'
```

```json
{
  "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](/integrations-api-third-party-sensors/reporting-api/occupancy.md) for what each field means.

## Request body

| Field           | Type       | Required                                | Description                                                                                                                                                                                                                                                               |
| --------------- | ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `measurements`  | `string[]` | Yes                                     | One or more [measurements](#measurements). No duplicates.                                                                                                                                                                                                                 |
| `items`         | `string[]` | Yes                                     | IDs of the entities to include (1–500). See [Items](/integrations-api-third-party-sensors/reporting-api/items-and-fields.md#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](/integrations-api-third-party-sensors/reporting-api/time-zones-and-filling-gaps.md). |
| `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](/integrations-api-third-party-sensors/reporting-api/items-and-fields.md#fields).                                                                                                                                        |
| `fill_gaps`     | `boolean`  | No                                      | Add an empty data point for every window that has no data. Default `false`. See [Filling gaps](/integrations-api-third-party-sensors/reporting-api/time-zones-and-filling-gaps.md#filling-gaps).                                                                          |

Fields not listed here are ignored. Send the body as JSON with `Content-Type: application/json`.

## Response

```json
{
  "data": {
    "<entity id>": [ { "start": "…", "stop": "…", "<field>": <value>, … }, … ]
  }
}
```

* **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](/integrations-api-third-party-sensors/reporting-api/errors.md).

## 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` | [Occupancy](/integrations-api-third-party-sensors/reporting-api/occupancy.md)                       |
| `spot_motion_occupancy`, `room_motion_occupancy`, `floor_motion_occupancy` | `spot_id` / `room_id` / `floor_id`   | `1m` `15m` `30m` `1h` `1d` | [Occupancy](/integrations-api-third-party-sensors/reporting-api/occupancy.md)                       |
| `dt_temperature`, `room_temperature`, `floor_temperature`                  | `device_id` / `room_id` / `floor_id` | `5m` `15m` `30m` `1h` `1d` | [Temperature](/integrations-api-third-party-sensors/reporting-api/temperature.md)                   |
| `room_door_window_activity`, `floor_door_window_activity`                  | `room_id` / `floor_id`               | `1m` `15m` `30m` `1h` `1d` | [Door/Window Activity](/integrations-api-third-party-sensors/reporting-api/door-window-activity.md) |
| `touch_events`                                                             | `room_id`                            | none (events)              | [Touch Events](/integrations-api-third-party-sensors/reporting-api/touch-events.md)                 |
| `cleaning_alert_events`                                                    | `room_id`                            | none (events)              | [Cleaning Alerts](/integrations-api-third-party-sensors/reporting-api/cleaning-alerts.md)           |
| `dt_heartbeat`                                                             | `device_id`                          | `15m` only                 | [Device Heartbeat](/integrations-api-third-party-sensors/reporting-api/device-heartbeat.md)         |

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](/integrations-api-third-party-sensors/reporting-api/query-limits.md)).
* **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**](/integrations-api-third-party-sensors/authentication.md#rate-limits) by batching measurements that have different keys and share a supported interval into one request.
