Appearance
Reading data out
Your system asks, Omaya answers. Read-only, on whatever schedule suits you.
The endpoints worth knowing
| Endpoint | Gives you |
|---|---|
GET /api/v1/spaces | Venues and their zones, with the UIDs everything else is addressed by |
GET /api/v1/occupancy | How many people are in every zone right now, each with its source |
GET /api/v1/occupancy/{zone_uid} | The same for one zone |
GET /api/v1/occupancy/{zone_uid}/history | Hourly peak, mean and last reading over a period |
GET /api/v1/gps/whereabouts?device_mac=… or ?entity=… | Where a GPS tracker, or the person it is bound to, is now: outside (geofence, trip or stop, position, map link), indoors (the room), or the last GPS position |
GET /api/v1/gps/points | Every kept GPS position of a tracker or person in a window (up to 7 days), with the geofence each fell in |
GET /api/v1/gps/activity | Time outside, distance, trips, stops and geofence visits per tracker and day — the Outdoor Activity report |
GET /api/v1/gps/geofences | Your geofences, as GeoJSON |
GET /api/v1/footfall | How many were here per day (or hour) and place — people and assets with a tag, door entries and exits, the most inside at once — the Footfall report |
GET /api/v1/attendance | When each person was on site, day by day: first in, last out, time on site, visits, time per floor — the Attendance report |
GET /api/v1/assets/utilisation | How much each tracked asset was used, where it spent its time, and which are idle or missing — the Asset Utilisation report |
GET /api/v1/incidents/response | How fast Security Watch incidents and panic presses were picked up and resolved, outcomes, per button and responder — the Panic & Incident Response report |
GET /api/v1/monitoring/status | Whether each gateway and sensor is up right now, for a network monitoring system — see Watching gateways and sensors from Zabbix |
GET /api/v1/gateways/availability | The share of each period every gateway was up, its outages, and whether it met your SLA — the Gateway Availability report |
Entities, admins, roles and users have their own endpoints too, and so does the Warehouse app: an ERP reads stock, orders, items, bins and the movement ledger from /api/v1/warehouse/…, with the same two headers, once the Warehouse app is switched on for your organisation. The Warehouse page explains it under Connecting your ERP. The API reference is the full list, with every parameter.
A first call
GET /api/v1/occupancy
X-Tenant-ID: yourorg
X-Api-Key: <your key>Each zone comes back with its count, the source that count came from, the tags seen regardless, its capacity and occupancy_pct if one is set, and a status of empty, normal, busy or over. Alongside the zones, venues gives one figure per building — see Whole-building figures.
Three things to build in from the start
Read the source, not just the count. A zone's source is one of four. radar and sensor mean everyone in the room; sensor includes counts your own systems push in. doors is the net of the counters on the room's own doors, used only where the room says every one of its doors is counted. tags means only people carrying one — a real number, answering a different question. A dashboard that shows all four as "occupancy" will mislead someone eventually.
Do not poll faster than the data changes. Live occupancy is cached for 30 seconds, so a request every second gets you the same answer thirty times. Once a minute is plenty for a wall display; a room-booking panel that needs to feel instant is the case for subscribing instead.
Ask the right endpoint about the right timeframe. Live endpoints are current to seconds. The history endpoint reads tables rebuilt every five minutes, and today's daily totals are provisional until the overnight rebuild. Neither is wrong; asking the second one about the last two minutes is.
Whole-building figures
venues in the same response gives each venue one headline count, from the first of these that applies:
source | The count is |
|---|---|
external | A building-wide count your badge or access-control system pushed in within the last ten minutes |
doors | The net of the venue's doorway counters — only when the venue says every way in and out is counted, and the counters are neither drifting apart nor stale |
zones | The sum of the venue's zones |
zones_sum is always the sum of the zones, whatever source says, so you can compare the two; the gap is mostly people in lobbies, corridors and other places no zone covers. For "how many people are in the building", use the venue's count, not the zone rows added up.
Reading history
GET /api/v1/occupancy/{zone_uid}/history returns one row per hour and per source, and each row carries three numbers that answer different questions:
| Field | Means |
|---|---|
peak | The busiest moment in that hour |
mean | The average across the hour's readings |
last | The final reading of the hour |
Use peak for "was this room ever too small", mean for "how hard did it work". Summing last across hours is meaningless — they are levels, not totals.
A zone can have two rows for the same hour
Each row's source is sensor (a radar, a counting sensor, or a count you pushed in) or gateway (BLE tags). A zone with both has two rows per hour, and adding them together counts the same people twice. Ask for one with ?source=sensor or ?source=gateway, or keep both and pick per hour.
Hours are UTC: hour is the start of the hour, so a building on UTC+8 finds its 9 am in the 01:00 row. from and to take ISO 8601 times and default to the last 24 hours. A range may span at most 31 days — a longer one is refused — so ask for a longer period in pieces.
Footfall and attendance
These two give the figures of the Footfall and Attendance reports, so what your system reads is what the report page shows. Dates are calendar days in your organisation's time zone, and times carry its offset (+08:00 and so on).
GET /api/v1/footfall answers "how many were here". level is how the answer is broken down: site (the whole organisation, the default), location, venue or zone. Add location_uid, venue_uid or zone_uid to look inside one place; level must then be that place's level or below it, so a venue takes venue or zone. from and to default to the last seven days and may span up to 92 days. granularity=hour gives one row per hour instead of per day.
GET /api/v1/footfall?level=venue&from=2026-09-29&to=2026-09-29
X-Tenant-ID: yourorg
X-Api-Key: <your key>json
{
"from": "2026-09-29", "to": "2026-09-29", "timezone": "Asia/Kuala_Lumpur",
"level": "venue", "granularity": "day",
"data": [
{ "date": "2026-09-29", "hour": null, "place_uid": "bbd83c2e-…", "place": "Level 4",
"devices": 8, "people": 8, "assets": 0, "entries": 40, "exits": 47,
"most_inside": 5, "most_inside_hour": 14, "has_counter": true,
"hours_with_data": 24, "hours_expected": 24 }
],
"totals": [
{ "place_uid": "acd7463f-…", "place": "Level 5", "visits": 13, "days_with_devices": 1,
"average_a_day": 13, "entries": 28, "exits": 7, "most_inside": 14,
"most_inside_at": "2026-09-29T18:00:00+08:00", "has_counter": true }
],
"unavailable": [], "excluded_counters": []
}devices counts registered tags only; people and assets split them by what the tag is on. entries, exits and most_inside come from door counters, so they count everyone, tagged or not — has_counter: false means the place has no counter, not that nobody came in. In totals, visits adds each day's devices up, so someone here every day counts once a day.
Two things look like "nobody" and are not. A date in unavailable had devices but no exact count at that level any more (the raw data behind it is kept about 30 days), so its devices is null rather than a guess. A row with hours_with_data: 0 means no data arrived at all that day — a power or network outage. Treat both as unknown, not zero.
GET /api/v1/attendance answers "who was on site, and when", for people only (entities of type person). from and to default to the last seven days and may span up to 31 days. venue_uid or location_uid counts only time in that place; entity_uid (the person's uid or identifier) narrows to one person. days=operating counts only your operating week's open days (see below); days=all, the default, counts every day.
GET /api/v1/attendance?from=2026-09-29&to=2026-09-29&days=operatingjson
{
"from": "2026-09-29", "to": "2026-09-29", "timezone": "Asia/Kuala_Lumpur", "gap_min": 30,
"open_days": { "applied": true, "configured": true, "label": "Mon-Fri", "mask": "1111100" },
"data": [
{ "entity_uid": "db6a2fdc-…", "identifier": null, "name": "Adib",
"here_now": false, "last_seen": "2026-09-29T17:45:15+08:00",
"days_present": 1, "off_days": 0, "expected_days": 1, "rate": 100,
"on_site_minutes": 500, "visits": 1,
"days": [
{ "date": "2026-09-29", "operating_day": true,
"first_in": "2026-09-29T09:25:11+08:00", "last_out": "2026-09-29T17:45:15+08:00",
"carried_in": false, "carried_out": false, "here_now": false,
"on_site_minutes": 500, "visits": 1, "outside_minutes": 0,
"venues": [ { "venue_uid": "bbd83c2e-…", "venue": "Level 4", "minutes": 496 },
{ "venue_uid": "acd7463f-…", "venue": "Level 5", "minutes": 3 } ] }
] }
],
"coverage": [ { "date": "2026-09-29", "hours_with_data": 24, "hours_expected": 24 } ]
}A gap of up to gap_min minutes (30 by default) between sightings is bridged, because someone walking a corridor no gateway hears is still on site; a longer gap, or time outside on GPS, starts a new visit. carried_in or carried_out means the person was already there at midnight or still there at the next one, so that first_in or last_out is midnight, not an arrival or a departure. Everyone with no time on site in the range is still listed, with empty days.
days_present out of expected_days is the person's attendance, and rate is that as a percentage (null when no day is counted). expected_days leaves out days that have not begun and days the gateways sent nothing — an outage is not an absence. With days=operating and an operating week set (System › Configuration › Organisation, or a venue's own week when you pass venue_uid), only open days count: a visit on a closed day stays in days with operating_day: false and is counted in off_days, never in days_present, on_site_minutes, visits or rate. open_days says what applied: applied: false means every day counted, either because you asked for days=all or because no week is set (configured: false). mask is seven 0/1 flags, Monday first.
A place or person that is not your organisation's is answered with 404, and a bad date, an over-long range or a level above the place you filter on with 422.
Assets, incidents and gateways
Three more report pages, with the same conventions as Footfall and Attendance: calendar days in your organisation's time zone, times with its offset, from and to defaulting to the last seven days and spanning at most 92 days, 404 for a place that is not yours and 422 for a bad date or range. Every figure is the report page's own.
GET /api/v1/assets/utilisation covers every asset (entities of type asset: trolleys, wheelchairs, forklifts). venue_uid or location_uid counts only time in that place; entity_uid (uid or identifier) narrows to one asset and adds its day-by-day figures. days=operating counts only the open days of your operating week, inside opening hours; days=all, the default, counts every day and every hour.
GET /api/v1/assets/utilisation?from=2026-09-29&to=2026-10-05json
{
"from": "2026-09-29", "to": "2026-10-05", "timezone": "Asia/Kuala_Lumpur", "missing_days": 3,
"open_days": { "applied": false, "configured": true, "label": null, "mask": "1111111" },
"totals": { "assets": 2, "tracked": 2, "seen": 2, "here_now": 0, "idle": 0, "missing": 0,
"moves": 14, "seen_minutes": 14592, "in_use_minutes": 163, "utilisation": 1.1 },
"groups": [ { "group_uid": null, "group": "No group", "assets": 2, "idle": 0, "missing": 0, "moves": 14, "utilisation": 1.1 } ],
"data": [
{ "entity_uid": "a1b910c9-…", "identifier": "Trolly-01", "name": "Trolley #1", "groups": [],
"has_tag": true, "seen_minutes": 7296, "in_use_minutes": 155, "moving_minutes": 0, "in_use_from": "motion",
"moves": 12, "utilisation": 2,
"base": { "place": "Sales area", "floor": "Level 5", "source": "most_time" },
"places": [ { "place": "Sales area", "floor": "Level 5", "minutes": 7121, "pct": 97.6, "base": true },
{ "place": "Meeting room & hall", "floor": "Level 4", "minutes": 147, "pct": 2, "base": false } ],
"here_now": false, "last_seen": "2026-10-06T18:02:21+08:00", "last_seen_place": "Sales area",
"last_seen_floor": "Level 5", "last_moved_at": null, "days_unseen": 0, "idle": false, "missing": false }
],
"coverage": [ { "date": "2026-09-29", "hours_with_data": 24, "hours_expected": 24 } ]
}"In use" is time at a stop away from its base (its home zone when one is set, source: home_zone, else the place it spent most time, most_time), plus the walk between stops, plus any minute its tag reported moving — so a trolley pushed about inside one room is in use. A minute counts once. moving_minutes is the motion part and in_use_from says what was read: motion (location and motion) or location (the tag reports no motion). A move is the stop changing place; a stop under five minutes is passing through and is dropped. idle needs no move and no moving minute. utilisation is in-use time inside the hours counted, as a percentage of those hours. idle means seen in the range but never moved; missing means not seen for more than missing_days days (3 unless you ask for another), counted from now whatever range you asked about. last_seen, last_seen_place and days_unseen are as of now too, so they can fall after to; here_now is only ever true for a range that includes the present. An asset with has_tag: false has no figures at all.
GET /api/v1/incidents/response reads Security Watch: every incident raised in the range, with venue_uid, type (panic, button, restricted and so on) or panic_only=true to narrow it. available: false means Security Watch is not set up for your organisation.
GET /api/v1/incidents/response?from=2026-09-07&to=2026-10-06&panic_only=truejson
{
"from": "2026-09-07", "to": "2026-10-06", "timezone": "Asia/Kuala_Lumpur", "available": true,
"totals": { "raised": 173, "critical": 172, "picked_up": 155, "never_picked_up": 18, "resolved": 152,
"still_open": 20, "unattended": 1, "reopened": 0, "false_alarm": 152, "false_alarm_rate": 100 },
"pick_up": { "count": 155, "of": 173, "median_sec": 15135, "p90_sec": 27435, "worst_sec": 280533,
"within": [ { "minutes": 2, "count": 1, "share_pct": 0.6 }, { "minutes": 5, "count": 2, "share_pct": 1.2 },
{ "minutes": 15, "count": 5, "share_pct": 2.9 } ] },
"resolve": { "count": 152, "of": 173, "median_sec": 15135, "p90_sec": 27133, "worst_sec": 31035, "within": [ … ] },
"outcomes": { "genuine": 0, "false_alarm": 152, "expected": 0, "test": 0, "duplicate": 0, "unattended": 1, "unknown": 0, "open": 20 },
"by_type": [ … ], "by_venue": [ … ], "by_zone": [ … ], "responders": [ … ],
"panic": { "incidents": 173, "presses": 173, "never_picked_up": 18, "buttons": [ … ], … },
"incidents": [
{ "uid": "8fd4506e-…", "detected_at": "2026-10-05T14:00:05+08:00", "type": "button", "severity": "warning",
"state": "resolved", "venue": "Level 5", "zone": "Main Meeting Room", "subject": "L5 S1 temperature sensor",
"title": "Button pressed on L5 S1 temperature sensor in Main Meeting Room — not a registered panic button",
"picked_up_after_sec": null, "resolved_after_sec": null, "outcome": "unattended", "reopened": 0,
"presses": 1, "picked_up_by": null, "taken_by": null, "resolved_by": null }
]
}Picked up means a person acknowledged the incident or resolved it, whichever came first. One the system closed as unattended, because nobody acted in time, was not picked up — so it counts in never_picked_up, not in picked_up. (The Rule & Alert Effectiveness page counts those closes as acknowledged, so its Security Watch figure is higher by exactly the unattended ones.) Response times are seconds; within gives how many were picked up inside 2, 5 and 15 minutes (resolved inside 15, 60 and 240). false_alarm_rate is false alarms out of the incidents judged genuine, false alarm or expected: test presses, duplicates, unattended and open ones are left out. In panic.buttons, registered: false is a press on a button nobody registered as a panic button, and carried_by is whoever it was bound to at the moment of the press.
GET /api/v1/gateways/availability gives each gateway's uptime. venue_uid or location_uid narrows it, gateway_uid (uid or MAC address) picks one, and sla_target sets the target in percent of all hours (99 unless you say).
GET /api/v1/gateways/availability?from=2026-09-01&to=2026-09-30json
{
"from": "2026-09-01", "to": "2026-09-30", "timezone": "Asia/Kuala_Lumpur", "sla_target": 99,
"records_begin": "2026-07-17T17:00:00+08:00", "resolution": "hour", "exact_from": null,
"data": [
{ "gateway_uid": "c1d9fac7-…", "name": "Dusun02", "mac": "30:AE:7B:EA:BC:4D", "type": "ble",
"location_uid": "7bf53941-…", "venue_uid": "bbd83c2e-…", "venue": "Level 4",
"status_now": "online", "last_seen": "2026-10-06T18:01:32+08:00",
"availability": 99.03, "operating_availability": 97.98,
"hours_counted": 720, "hours_up": 713, "hours_down": 7, "outages": 2,
"resolution": "hour", "exact_from": "2026-10-06T18:00:00+08:00",
"mtbf_hours": 356.5, "mttr_hours": 3.5, "meets_sla": true, "down_now": false,
"longest": { "start": "2026-09-09T08:00:00+08:00", "end": "2026-09-09T13:00:00+08:00",
"down_minutes": 300, "duration_minutes": 300, "resolution": "hour", "cause": null,
"ongoing": false, "began_before": false },
"outage_list": [ … ] }
],
"rollups": {
"site": { "uid": null, "name": null, "gateways": 10, "hours_counted": 6114, "hours_up": 5835, "hours_down": 279,
"availability": 95.44, "operating_availability": 94.82, "outages": 7, "below_sla": 3, "meets_sla": false },
"locations": [ … ], "venues": [ … ]
},
"excluded": { "disabled_gateways": 0, "hours_not_in_service": 85 }
}Outages are exact from when each gateway's outage log began (exact_from, from 6 October 2026): an outage runs from the gateway's last packet to its first packet after, so twenty minutes down costs twenty minutes and hours_down is a decimal. Before that the report works to the hour: an hour in which the gateway passed on anything from one of your devices is up, an hour with nothing is down, so a short outage inside an hour that also carried traffic does not show and a working gateway with nothing near it to hear reads as down. resolution says which applies — exact, hour, or mixed for a range that spans both — and every outage carries its own. cause: all_gateways means every gateway of the site went silent together: look at power or the network, not the gateway. down_minutes is the part inside the range, duration_minutes the whole outage. Time that does not count at all: before the gateway was added or after it was removed, before records begin (records_begin), its quiet hours, and the current hour, which has not finished. operating_availability is the same over opening hours on open days only (days=all treats every day as open). end is null while an outage is still going. The roll-ups add time together; they never average the gateways' percentages. status_now is the live state, for a dashboard that shows both — it says nothing about the period.
When the answer is empty
An empty zone list usually means one of three things, in order of likelihood: the key belongs to a different organisation than the one in X-Tenant-ID; the zones exist but nothing in them counts anything; or the zones have not been drawn yet. The occupancy reference covers the middle case, which is the one that looks most like a bug and is not.