Choosing the right API
Each data surface is designed for a different job. A few minutes here will help you pick the right one for your workload before you build.
Which API for which job
| You want | Use | Instead of |
|---|---|---|
| Live occupancy pushed to your system | Webhooks (OccupancyChanges) | Polling analytics |
| Live/recent occupancy on demand | occupancyChanges / occupancy queries (polling guide) | occupancyPerMinute, which is next-day analytics |
| Utilisation reporting over days/weeks/months | Analytics queries (occupancyPerHour, occupancyPerDay, ...) | Storing and aggregating every webhook event yourself |
| Where activity happened on a floor plan over a period | Historical heatmap | Reconstructing it from sightings |
| Raw event-level history beyond the retention windows below | Webhooks, captured and stored on your side |
The API focuses on recent events plus long-term aggregates. Occupancy changes are retained for up to 24 hours (or the most recent 1,000,000 changes), space sensor readings for up to 48 hours, and floor sightings for up to 24 hours. Beyond those windows the API serves aggregates (analytics and heatmaps). If your application needs raw events long-term, capture them via webhooks and store them from day one, since raw events age out of the API after these windows.
Feature enablement
Each data surface is a feature that XY Sense enables for your organisation, and enablement can vary per building and floor. Before building against a surface, confirm with your XY Sense representative that it is enabled where you need it.
| Surface | Feature |
|---|---|
Occupancy queries and OccupancyChanges webhooks | Occupancy API |
Floor sighting queries and FloorSightings webhooks | Sightings API |
Space sensor reading queries and SpaceSensorReadings webhooks | Space Sensor Readings API |
| Analytics queries and historical heatmaps | Analytics API |
| Test-data generator mutations | Dev mode (per floor) |
When a feature is not enabled, queries return a readable error naming the feature and the affected floors (for example Analytics Api access disabled for floor ...), and webhook registration for the matching event type is declined with a similar message. Running the Quickstart queries is a quick way to smoke-test your access.
Live data vs analytics
- Occupancy changes, sightings, and sensor readings are near-real-time, with updates as often as every ~2 seconds.
- Analytics (
occupancyPerMinute,occupancyPerFiveMinutes,occupancyPerHour,occupancyPerDay) is processed overnight per building. The previous day's data becomes available at approximately 6 am the following day in the building's local time.occupancyPerMinuteis minute-resolution analytics rather than a live feed, and like all analytics it becomes available the next day. - Historical heatmaps run approximately 15 minutes behind live.
Timestamps are UTC
All API timestamps are UTC in ISO 8601 format (2026-06-02T04:17:39.106Z), and all from/to arguments are interpreted as UTC. If API results seem to disagree with the dashboard, the timezone of your from/to arguments is the first thing to check.
Worked example: to request analytics for Tuesday 3 June 2026 in Sydney (AEST, UTC+10), query from: "2026-06-02T14:00:00Z" to: "2026-06-03T14:00:00Z".
Field meanings:
collectedDate(occupancy) is when the underlying observation was made. Use this as the event time.occupancyStatusChangeDate(occupancy) is when the space last changed occupancy state. It stays the same while headcount varies within the same state.collectedAt(space sensor readings) has the same meaning ascollectedDate. The name differs for historical reasons only.
Missing analytics rows mean zero
Analytics intervals with no occupancy are omitted from results rather than returned as zero rows (a deliberate storage optimisation). When aggregating downstream, treat absent intervals for a known space as zero occupancy rather than missing data.
Related: results reflect the walk-by filter (brief pass-through detections are excluded from occupied time) and any configured active-hours filtering. See Available data for the metric definitions.
Paging analytics results
Analytics queries return offset pages (skip/take, max page size 100,000 records). A year of hourly data for a large portfolio spans multiple pages, so loop until hasNextPage is false:
query HourlyPage($buildingId: ID, $from: Date!, $to: Date!, $skip: Int, $take: Int) {
occupancyPerHour(buildingId: $buildingId, from: $from, to: $to, skip: $skip, take: $take) {
totalItems
hasNextPage
items {
floorSpaceId
date
hour
avgOccupants
maxOccupants
occupiedSeconds
}
}
}
First page: { "skip": 0, "take": 50000 }. While the response has "hasNextPage": true, request the next page with { "skip": 50000, "take": 50000 }, and so on. totalItems lets you size the loop up front.