Skip to main content

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 wantUseInstead of
Live occupancy pushed to your systemWebhooks (OccupancyChanges)Polling analytics
Live/recent occupancy on demandoccupancyChanges / occupancy queries (polling guide)occupancyPerMinute, which is next-day analytics
Utilisation reporting over days/weeks/monthsAnalytics queries (occupancyPerHour, occupancyPerDay, ...)Storing and aggregating every webhook event yourself
Where activity happened on a floor plan over a periodHistorical heatmapReconstructing it from sightings
Raw event-level history beyond the retention windows belowWebhooks, 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.

SurfaceFeature
Occupancy queries and OccupancyChanges webhooksOccupancy API
Floor sighting queries and FloorSightings webhooksSightings API
Space sensor reading queries and SpaceSensorReadings webhooksSpace Sensor Readings API
Analytics queries and historical heatmapsAnalytics API
Test-data generator mutationsDev 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. occupancyPerMinute is 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 as collectedDate. 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.