# Alerts

> Alert types, severity and states; the difference between the control room taking charge and the worker seeing the alert; and how to acknowledge or close it.

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

An **alert** is something that has happened in the plant and that someone has to deal with: an SOS, a fall, someone entering where they shouldn’t, a watch with a flat battery or an earthquake warning. It’s the central object of the API.

## The object

The alert arrives complete in every webhook (`data.alert`) and is what `GET /v1/alerts/{id}` returns. The most commonly used fields:

| Field | What it is |
| --- | --- |
| `type`, `severity`, `status` | What has happened, how serious it is and where it stands in the response. |
| `title` | The title the control room reads. |
| `site`, `zone` | Where. `zone` can be `null`. |
| `worker`, `device` | Who it happened to and with which watch. `null` in plant-wide alerts. |
| `location`, `location_withheld` | Where the person is, if the plant’s privacy level allows it to be seen. |
| `details` | Data specific to each type. |
| `acknowledged`, `resolved`, `assigned_to` | Who has taken charge, who has closed it and who it is assigned to. |
| `date_created`, `date_last_modified` | When it was created and when it last changed. |
| `simulated` | `true` if it comes from a drill or from simulated watches. |
| `panel_url` | Link to its page in the SafeOnuba dashboard. |

The full list, field by field, is in the schema [`Alert`](https://safeonuba.com/en/developers/reference/schemas#Alert).

## Types

| type | What it means |
| --- | --- |
| `sos` | The person has asked for help from the watch, or the control room has raised it on their behalf. |
| `fall_detected` | The watch has detected a fall. |
| `restricted_zone_entry` | Entry into a restricted zone. |
| `forbidden_zone_entry` | Entry into an off-limits zone. |
| `permit_expired_inside` | The permit to work has expired with the person still inside the zone. |
| `permit_closed_inside` | The permit to work has been closed with the person still inside the zone. |
| `heat_stress` | Heat stress: the watch has told them to stop and rest. |
| `evacuation_help` | During an evacuation, the person says from the watch that they can’t get out on their own or that they are helping someone else. |
| `worker_call_request` | The person alerts the control room from the watch. Without coverage, the watch records a voice message that can be heard in the dashboard. |
| `device_offline` | The watch hasn’t communicated for a while. |
| `low_battery` | The watch is low on battery. |
| `earthquake` | Earthquake. It is plant-wide: it carries no `worker` or `device`. |
| `tsunami_risk` | Tsunami risk. It is plant-wide: it carries no `worker` or `device`. |

### A nearby vehicle is not an alert

Nearby-vehicle warnings don’t generate alerts of their own. They go inside the alert for an incident, in two lists:

- `nearby`: who was within 100 metres when a critical alert (SOS or fall) was raised, people or vehicles. It gives the distance, never the position, and doesn’t count anyone who was in a privacy zone.
- `vehicle_warnings`: the nearby-vehicle warnings that person received in the incident window, with the distance and whether they saw them on the watch.

### Details for each type

`details` changes according to `type`. For example, [`SosDetails`](https://safeonuba.com/en/developers/reference/schemas#SosDetails) says how the SOS was raised (`trigger`), [`ZoneDetails`](https://safeonuba.com/en/developers/reference/schemas#ZoneDetails) carries the permit to work that applied (`permit_reference`) and [`HazardDetails`](https://safeonuba.com/en/developers/reference/schemas#HazardDetails) the magnitude, the distance and whether the warning is official. The variants are in [`AlertDetails`](https://safeonuba.com/en/developers/reference/schemas#AlertDetails).

> **There are never vital signs**
>
> Not in `details` and not anywhere else. [`HeatStressDetails`](https://safeonuba.com/en/developers/reference/schemas#HeatStressDetails) says what the worker has been told (`stop_and_rest`), the plant’s heat index and the minutes in the sun, nothing that comes from their pulse.

## Severity

| severity | What it means |
| --- | --- |
| `low` | Information. |
| `medium` | Warning. |
| `high` | Serious. |
| `critical` | Critical: SOS, fall, “I can’t evacuate”. |

The `severity` filter of `GET /v1/alerts` and the `min_severity` of webhooks are **minimums**: `high` returns `high` and `critical`.

## States

| status | What it means |
| --- | --- |
| `unacknowledged` | Nobody has taken charge yet. |
| `acknowledged` | An operator has taken charge. |
| `resolved` | Closed. |

- **Acknowledging** assigns the alert to whoever acknowledges it, if nobody was handling it. Acknowledging is not resolving: an acknowledged alert keeps marking the person on the map until it is resolved.
- **Resolving** records who, when and with what note, in `resolved`.
- A resolved alert can be **reopened**. The `alert.reopened` event announces it.

> **Two different “seen”**
>
> `status: "acknowledged"` means that a control room operator has taken charge. `worker_acknowledged_at` is something else: the time (from the watch) at which the worker tapped “Got it” on their wrist. Each has its own event: `alert.acknowledged` and `alert.worker_acknowledged`.

## Acknowledging and resolving from your control room

`PUT /v1/alerts/{id}` with the `alerts:write` scope. The body:

| Field | Required | What it is |
| --- | --- | --- |
| `status` | Yes | `acknowledged` or `resolved`. |
| `actor` | Yes | Your control room’s operator: `name` and, if you want, `external_id`. |
| `resolution_reason` | No | When resolving, why. Up to 200 characters. |
| `note` | No | A note. Up to 500 characters. |

The operator is required because “acknowledged” measures a person’s response. In the alert’s timeline it appears as “Customer · Operator (via integration)”.

```bash
curl -X PUT https://sandbox.api.safeonuba.com/v1/alerts/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90 \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "resolved",
    "resolution_reason": "False alarm",
    "note": "Confirmed by radio with the supervisor.",
    "actor": { "name": "E. Example", "external_id": "OP-117" }
  }'
```

```javascript
const res = await fetch(`https://sandbox.api.safeonuba.com/v1/alerts/${alertId}`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${await safeOnubaToken()}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 'resolved',
    resolution_reason: 'False alarm',
    note: 'Confirmed by radio with the supervisor.',
    actor: { name: currentOperator.name, external_id: currentOperator.id },
  }),
});
if (res.status === 409) {
  // It was already resolved, or the transition isn't possible: nothing to do.
} else if (!res.ok) {
  throw new Error(`${res.status}: ${await res.text()}`);
}
```

```python
res = requests.put(
    f"https://sandbox.api.safeonuba.com/v1/alerts/{alert_id}",
    headers={"Authorization": f"Bearer {safeonuba_token()}"},
    json={
        "status": "resolved",
        "resolution_reason": "False alarm",
        "note": "Confirmed by radio with the supervisor.",
        "actor": {"name": current_operator.name, "external_id": current_operator.id},
    },
    timeout=10,
)
if res.status_code != 409:  # 409: it was already resolved
    res.raise_for_status()
```

```csharp
var body = JsonContent.Create(new
{
    status = "resolved",
    resolution_reason = "False alarm",
    note = "Confirmed by radio with the supervisor.",
    actor = new { name = currentOperator.Name, external_id = currentOperator.Id },
});
var res = await http.PutAsync($"https://sandbox.api.safeonuba.com/v1/alerts/{alertId}", body);
if (res.StatusCode != System.Net.HttpStatusCode.Conflict) res.EnsureSuccessStatusCode();
```

The response is the updated alert. If the transition isn’t possible —for example, resolving or acknowledging an alert that is already resolved— the API responds `409`.

## Querying alerts

`GET /v1/alerts` returns alerts from newest to oldest, paginated. Filters:

| Parameter | What it filters |
| --- | --- |
| `site_id` | A site. |
| `type` | A type. |
| `status` | A state. |
| `severity` | Minimum severity. |
| `since`, `until` | Date range, in ISO 8601. |
| `limit`, `cursor` | Pagination (see [Pagination](https://safeonuba.com/en/developers/pagination)). |

To follow alerts in real time, don’t poll in a loop: use [webhooks](https://safeonuba.com/en/developers/webhooks). The list is for loading the state at start-up and for searching.

## Worker names

Names are optional by contract. If your contract doesn’t include them, `worker.name` doesn’t appear and `title` is built from the type, the zone and the company identifier (`external_id`), without a name. See [Workers and watches](https://safeonuba.com/en/developers/concepts/workers-and-watches).
