# Positions

> When the API returns a position, what location_withheld means, how privacy zones are represented and how accurate each point is.

Page: https://safeonuba.com/en/developers/concepts/positions · SafeOnuba API v1 (`api_version: "2026-09-24"`), in preview: the contract may change.

A position is where a person is. It’s the most sensitive data in the API, and that’s why it follows exactly the same rule as the dashboard: if a person in the dashboard with those permissions can’t see it, neither can the API.

## When a position is visible

It is decided by each site’s privacy level (`privacy_mode` in `GET /v1/sites`):

- **“Emergencies only”** (`on_demand`), the default value. A person’s position is only visible when they have an open alert with severity equal to or above `unveil_min_severity`, during an evacuation or with a request approved in the dashboard.
- **Continuous** (`continuous`). The plant has chosen to see positions continuously.

Inside a privacy zone (changing rooms, canteens), nobody’s position is recorded, at any level.

## In an alert: location_withheld

Every alert carries `location` and `location_withheld`:

- If the privacy level allows the position to be seen, `location` carries it and `location_withheld` is `false`.
- If the position exists but isn’t delivered —in “emergencies only”, an alert below the severity that lifts the veil—, `location_withheld` is `true`. It isn’t visible in the dashboard either.

Your control room has to handle both cases. An alert without a position isn’t an error: it’s the plant protecting its people. For that case, `panel_url` leads to the alert’s page in the dashboard.

## Latest positions

`GET /v1/positions/latest` (scope `positions:read`) returns the positions that the privacy level allows to be seen **at that moment**. In “emergencies only”, anyone with nothing open doesn’t appear.

| Field | What it is |
| --- | --- |
| `device_id` | The watch. |
| `worker` | The person. |
| `location` | The position. |
| `in_vehicle` | Whether they are in a vehicle. |

Every position served with an alert behind it is recorded in that alert, under “Who has seen their position”.

## The location object

| Field | What it is |
| --- | --- |
| `latitude`, `longitude` | WGS84 coordinates. |
| `uncertainty_m` | Radius of uncertainty in metres. `null` if unknown, which isn’t the same as having it perfect. |
| `source` | Where the point comes from. |
| `masked` | `true` if the point is the representative one for a privacy zone and not the person’s own. |
| `position_date_utc` | When it was taken. |
| `height` | Height above the ground by barometer, with its uncertainty, building and floor, when it can be stated. Otherwise, `null`. |

| source | What it means |
| --- | --- |
| `gps` | The watch’s GPS. |
| `network` | Position by network. |
| `zone` | The person was in a privacy zone: the point is the zone’s, not theirs. |

Always draw the `uncertainty_m` radius next to the point: a point without a radius gives a precision it doesn’t have.

## Distances without a position

In a critical alert, `nearby` says who was within 100 metres and at what distance, **never where**. Anyone who was in a privacy zone doesn’t count. See [Alerts](https://safeonuba.com/en/developers/concepts/alerts#a-nearby-vehicle-is-not-an-alert).

## Don’t track anyone in a loop

Querying `GET /v1/positions/latest` every few seconds gives you nothing more than the alerts: in “emergencies only” only people who already have an open alert appear, and that alert already reaches you by webhook with the position inside. On top of that, every query counts towards your [rate limits](https://safeonuba.com/en/developers/rate-limits).
