# Webhooks · Reference

> Destinations and the format of what is sent to them.

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

## List destinations

`GET /v1/webhooks` · scope `webhooks:manage` · [see it on the web](https://safeonuba.com/en/developers/reference/webhooks#listWebhooks)

### Responses

- `200`: Destinations. → `WebhookEndpointList` (`application/json`)
- `401`: The token is missing, has expired or has been revoked. → `Problem` (`application/problem+json`)
- `403`: The token lacks the scope the route requires, or the site doesn’t belong to the client. → `Problem` (`application/problem+json`)
- `429`: Too many requests, or too much query time: each client has a number of requests per minute (600 by default) and a query time per minute (30 s by default), and each request spends however long it takes, so a long list counts for more than a single alert. `Retry-After` says when to retry. → `Problem` (`application/problem+json`)

### Example request (sandbox)

```bash
curl "https://sandbox.api.safeonuba.com/v1/webhooks" \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN"
```

### Example response (`200`)

```json
{
  "data": [
    {
      "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
      "url": "string",
      "event_types": [
        "alert.created"
      ],
      "filters": {
        "site_ids": [
          "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
        ],
        "min_severity": "low",
        "contractors": [
          "string"
        ]
      },
      "is_active": false,
      "failing_since": "2026-09-24T11:04:12Z",
      "created_at": "2026-09-24T11:04:12Z"
    }
  ]
}
```

## Create a destination

`POST /v1/webhooks` · scope `webhooks:manage` · [see it on the web](https://safeonuba.com/en/developers/reference/webhooks#createWebhook)

### Body (`application/json`, schema `WebhookEndpointCreate`)

- `url` (string, required)
- `event_types` (array<EventType | "alert.*" | "muster.*">, required)
- `filters` (object)
  - `site_ids` (array<string (uuid)>)
  - `min_severity` (Severity) Values: `low`, `medium`, `high`, `critical`.
  - `contractors` (array<string>)

### Responses

- `201`: Destination created, with its signing secret (this once only). → `WebhookEndpointCreated` (`application/json`)
- `401`: The token is missing, has expired or has been revoked. → `Problem` (`application/problem+json`)
- `403`: The token lacks the scope the route requires, or the site doesn’t belong to the client. → `Problem` (`application/problem+json`)
- `422`: The body is not valid (the URL must be https). → `Problem` (`application/problem+json`)
- `429`: Too many requests, or too much query time: each client has a number of requests per minute (600 by default) and a query time per minute (30 s by default), and each request spends however long it takes, so a long list counts for more than a single alert. `Retry-After` says when to retry. → `Problem` (`application/problem+json`)

### Example request (sandbox)

```bash
curl -X POST "https://sandbox.api.safeonuba.com/v1/webhooks" \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://sala.cliente.example/safeonuba",
  "event_types": [
    "alert.*",
    "muster.*"
  ]
}'
```

### Example response (`201`)

```json
{
  "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "url": "string",
  "event_types": [
    "alert.created"
  ],
  "filters": {
    "site_ids": [
      "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
    ],
    "min_severity": "low",
    "contractors": [
      "string"
    ]
  },
  "is_active": false,
  "failing_since": "2026-09-24T11:04:12Z",
  "created_at": "2026-09-24T11:04:12Z",
  "secret": "whsec_6f1c0d9e2b7a4c3f8e5d1a0b9c8e7f6a"
}
```

## Delete a destination

`DELETE /v1/webhooks/{id}` · scope `webhooks:manage` · [see it on the web](https://safeonuba.com/en/developers/reference/webhooks#deleteWebhook)

### Path parameters

- `id` (string (uuid), required): Destination id.

### Responses

- `204`: Deleted.
- `401`: The token is missing, has expired or has been revoked. → `Problem` (`application/problem+json`)
- `403`: The token lacks the scope the route requires, or the site doesn’t belong to the client. → `Problem` (`application/problem+json`)
- `404`: It doesn’t exist. → `Problem` (`application/problem+json`)
- `429`: Too many requests, or too much query time: each client has a number of requests per minute (600 by default) and a query time per minute (30 s by default), and each request spends however long it takes, so a long list counts for more than a single alert. `Retry-After` says when to retry. → `Problem` (`application/problem+json`)

### Example request (sandbox)

```bash
curl -X DELETE "https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90" \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN"
```

## Rotate the signing secret

`POST /v1/webhooks/{id}/rotate-secret` · scope `webhooks:manage` · [see it on the web](https://safeonuba.com/en/developers/reference/webhooks#rotateWebhookSecret)

Issues a new secret. The previous one keeps signing for 24 h: during that time every delivery carries both signatures in `X-SafeOnuba-Signature`, separated by a comma, so the receiver can change secret without interruption. A second rotation retires the first: there are never more than two.

### Path parameters

- `id` (string (uuid), required): Destination id.

### Responses

- `200`: The new secret (this once only) and until when the previous one signs. → `WebhookSecretRotated` (`application/json`)
- `401`: The token is missing, has expired or has been revoked. → `Problem` (`application/problem+json`)
- `403`: The token lacks the scope the route requires, or the site doesn’t belong to the client. → `Problem` (`application/problem+json`)
- `404`: It doesn’t exist. → `Problem` (`application/problem+json`)
- `429`: Too many requests, or too much query time: each client has a number of requests per minute (600 by default) and a query time per minute (30 s by default), and each request spends however long it takes, so a long list counts for more than a single alert. `Retry-After` says when to retry. → `Problem` (`application/problem+json`)

### Example request (sandbox)

```bash
curl -X POST "https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/rotate-secret" \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN"
```

### Example response (`200`)

```json
{
  "secret": "whsec_6f1c0d9e2b7a4c3f8e5d1a0b9c8e7f6a",
  "previous_valid_until": "2026-09-24T11:04:12Z"
}
```

## Send a test event

`POST /v1/webhooks/{id}/test` · scope `webhooks:manage` · [see it on the web](https://safeonuba.com/en/developers/reference/webhooks#testWebhook)

Sends an `alert.created` marked `simulated: true`, signed like a real one.

### Path parameters

- `id` (string (uuid), required): Destination id.

### Responses

- `202`: Queued: the result shows on the destination (`failing_since`).
- `401`: The token is missing, has expired or has been revoked. → `Problem` (`application/problem+json`)
- `403`: The token lacks the scope the route requires, or the site doesn’t belong to the client. → `Problem` (`application/problem+json`)
- `404`: It doesn’t exist. → `Problem` (`application/problem+json`)
- `429`: Too many requests, or too much query time: each client has a number of requests per minute (600 by default) and a query time per minute (30 s by default), and each request spends however long it takes, so a long list counts for more than a single alert. `Retry-After` says when to retry. → `Problem` (`application/problem+json`)

### Example request (sandbox)

```bash
curl -X POST "https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/test" \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN"
```

# What your destination receives

Each event reaches your URL as a `POST` with this body. Both groups share the format: what changes is what goes inside `data`.

## Changes to an alert

Your destination receives a `POST` for each `alert.*` event. It is signed; it carries no token.

One `POST` per event to each subscribed destination, with the complete object.

Headers: `X-SafeOnuba-Event-Id`, `X-SafeOnuba-Timestamp` (Unix seconds) and
`X-SafeOnuba-Signature: v1=<hex(HMAC_SHA256(secret, timestamp + "." + body))>`. Reject
anything more than 5 min out of sync. During a rotation two signatures arrive, separated by a comma.

Acknowledgement: any `2xx` in under 5 s. Retries at 10 s, 30 s, 2 min, 10 min, 30 min,
1 h and then every hour up to 24 h. “At least once” delivery (deduplicate by `id`) and **no guaranteed
order** (use `date_last_modified`). SOS, falls, “I can’t evacuate” and evacuations go out
before the rest.

### Body (`application/json`, schema `Event`)

- `id` (string (uuid), required): For deduplication: delivery is “at least once”.
- `type` (EventType, required) Values: `alert.created`, `alert.acknowledged`, `alert.resolved`, `alert.reopened`, `alert.worker_acknowledged`, `muster.started`, `muster.kind_changed`, `muster.phase_changed`, `muster.targets_changed`, `muster.updated`, `muster.worker_missing`, `muster.ended`.
- `api_version` (string, required) Values: `2026-09-24`.
- `created_at` (string (date-time), required)
- `organization_id` (string (uuid) | null, required): The site’s company. `null` in test events.
- `site_id` (string (uuid), required)
- `data` ({ alert } | { muster }, required): The complete object exactly as the `GET` would return it at that moment.
  - Variant 1:
    - `alert` (Alert, required)
      - `id` (string (uuid), required)
      - `type` (AlertType, required) Values: `sos`, `fall_detected`, `restricted_zone_entry`, `forbidden_zone_entry`, `permit_expired_inside`, `permit_closed_inside`, `heat_stress`, `evacuation_help`, `worker_call_request`, `device_offline`, `low_battery`, `earthquake`, `tsunami_risk`.
      - `severity` (Severity, required) Values: `low`, `medium`, `high`, `critical`.
      - `status` (AlertStatus, required) Values: `unacknowledged`, `acknowledged`, `resolved`.
      - `title` (string, required): With `include_worker_names`, the title the control room reads (it carries the person’s name). Without it, one composed of the type, the zone and the `external_id`, with no name.
      - `date_created` (string (date-time), required)
      - `date_last_modified` (string (date-time), required): For ordering: webhooks don’t guarantee order, and anything older than what you already have is discarded.
      - `site` (SiteRef, required)
        - `id` (string (uuid), required)
        - `name` (string, required)
      - `zone` (ZoneRef, required)
        - `id` (string (uuid), required)
        - `name` (string, required)
      - `worker` (WorkerRef, required)
        - `id` (string (uuid), required)
        - `external_id` (string | null, required): The worker’s code at the company (the one in its personnel system). `null` if not given.
        - `name` (string): Full name. **Only** if the contract includes it; otherwise the property doesn’t appear.
        - `contractor` (string | null, required)
      - `device` (DeviceRef, required)
        - `id` (string (uuid), required)
        - `serial` (string, required)
        - `battery` (integer | null, required) (from 0 to 100)
        - `date_last_seen` (string (date-time) | null, required)
      - `location` (Location, required)
        - `latitude` (number, required) (from -90 to 90)
        - `longitude` (number, required) (from -180 to 180)
        - `uncertainty_m` (number | null, required): Uncertainty radius in metres. `null` if unknown: not knowing it is not the same as it being perfect. (minimum 0)
        - `source` (string, required): `zone`: the person was in a privacy zone and the point is the zone’s, not theirs. Values: `gps`, `network`, `zone`.
        - `masked` (boolean, required): The point is the representative one for a privacy zone.
        - `position_date_utc` (string (date-time), required)
        - `height` (object | null, required): Height above ground by barometer, when it can be stated; `null` if not.
      - `location_withheld` (boolean, required): The position exists but is not delivered: in “emergencies only”, an alert below the severity that lifts the veil doesn’t show it, and neither does the dashboard.
      - `details` (AlertDetails, required)
        - Variant `SosDetails`:
          - `trigger` (string, required): How it was requested: on the watch screen, with the physical button, after an unanswered fall, or from the control room. Values: `watch_button`, `watch_hardware_button`, `after_fall`, `control_room`.
        - Variant `FallDetails`:
          - `escalated_to_sos` (boolean, required): Did not answer the watch’s “Are you OK?” and it escalated to SOS.
        - Variant `ZoneDetails`:
          - `zone_type` (string, required) Values: `restricted`, `forbidden`.
          - `permit_reference` (string | null, required): The permit to work (job safety analysis, JSA) in force, if there was one.
        - Variant `HeatStressDetails`:
          - `action` (string, required): What the watch has told them: stop, drink and find shade. There are no levels. Values: `stop_and_rest`.
          - `heat_index_c` (number | null, required): The plant’s heat index (°C).
          - `sun_exposure_minutes` (integer | null, required)
        - Variant `EvacuationHelpDetails`:
          - `reason` (string | null, required): What they said on the watch: trapped, injured or helping someone else. Values: `trapped`, `injured`, `helping`.
          - `muster_id` (string (uuid), required)
        - Variant `WorkerCallDetails`:
          - `voice_note` (boolean, required): With no coverage, the watch recorded a voice message (it can be heard in the dashboard).
        - Variant `DeviceOfflineDetails`:
          - `minutes_silent` (integer, required)
        - Variant `LowBatteryDetails`:
          - `battery_pct` (integer, required) (from 0 to 100)
        - Variant `HazardDetails`:
          - `magnitude` (number | null, required)
          - `distance_km` (number | null, required)
          - `official` (boolean, required): `false`: a SafeOnuba estimate with no official warning. `true`: an official bulletin confirmed by a person.
          - `felt_at_site` (boolean, required)
          - `eta` (string (date-time) | null, required): Estimated arrival of the wave (tsunami).
      - `nearby` (array<Nearby>, required)
        - `kind` (string, required) Values: `person`, `vehicle`.
        - `worker` (object, required)
        - `vehicle_type` (string | null, required)
        - `distance_m` (integer, required)
        - `reported_at` (string (date-time), required)
      - `vehicle_warnings` (array<VehicleWarning>, required)
        - `at` (string (date-time), required)
        - `vehicle_type` (string | null, required)
        - `distance_m` (integer, required)
        - `worker_acknowledged_at` (string (date-time) | null, required)
      - `worker_acknowledged_at` (string (date-time) | null, required): The worker pressed “Got it” on their watch (watch time).
      - `acknowledged` (ActionBy, required)
        - `at` (string (date-time), required)
        - `by` (string, required): Who: a person from the dashboard, or “Client · Operator (via integration)”.
      - `resolved` (object | null, required): Resolved: by whom, when and with what note.
        - `at` (string (date-time), required)
        - `by` (string, required): Who: a person from the dashboard, or “Client · Operator (via integration)”.
        - `note` (string | null, required)
      - `assigned_to` (string | null, required)
      - `simulated` (boolean, required): Raised in a drill or with simulated watches.
      - `panel_url` (string (uri), required)
  - Variant 2:
    - `muster` (Muster, required)
      - `id` (string (uuid), required)
      - `site` (SiteRef, required)
        - `id` (string (uuid), required)
        - `name` (string, required)
      - `status` (string, required) Values: `active`, `completed`, `cancelled`.
      - `phase` (string, required): `inspecting`: headcount complete and the control room is checking the plant; watches say “do not go back until further notice”. Values: `evacuating`, `inspecting`.
      - `kind` (MusterKind, required) Values: `general`, `tsunami`, `earthquake`.
      - `reason` (string | null, required)
      - `started_at` (string (date-time), required)
      - `ended_at` (string (date-time) | null, required)
      - `deadline` (object | null, required)
        - `at` (string (date-time), required)
        - `source` (string, required): Tsunami: the time of an official warning, or the ceiling in the zone’s plan. Never a physical calculation. Values: `official`, `plan_estimate`.
      - `origin` (object | null, required): Where the emergency comes from. `null` in an evacuation with no origin (a general drill).
        - `latitude` (number, required)
        - `longitude` (number, required)
        - `radius_m` (integer, required)
        - `label` (string | null, required)
        - `zone` (ZoneRef, required)
      - `targets_revision` (integer, required): Goes up every time the targets change (ruling out, restoring, change of type).
      - `totals` (object, required): The same figures as the dashboard banner and the closing summary: the server counts them. In an ended evacuation, what remains is what was recorded on ending —arrived and total— and the rest comes out as `null`.
        - `expected` (integer, required)
        - `safe` (integer, required)
        - `help` (integer | null, required)
        - `wrong_point` (integer | null, required)
        - `pending` (integer | null, required)
        - `no_signal` (integer | null, required)
        - `not_worn` (integer | null, required)
      - `assembly_points` (array<AssemblyPoint>, required): The targets for the type, ruled-out ones included.
        - `id` (string (uuid), required)
        - `name` (string, required)
        - `status` (string, required) Values: `available`, `excluded`.
        - `count` (integer | null, required): People safe at this point. `null` in an ended evacuation: on ending, only the total is kept.
        - `excluded` (object | null, required)

### Responses

- `200`: Received. Any `2xx` will do.

### Example body

```json
{
  "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "type": "alert.created",
  "api_version": "2026-09-24",
  "created_at": "2026-09-24T11:04:12Z",
  "organization_id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "site_id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "data": {
    "alert": {
      "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
      "type": "sos",
      "severity": "critical",
      "status": "unacknowledged",
      "title": "SOS activado — Ana Ejemplo",
      "date_created": "2026-09-24T11:04:12Z",
      "date_last_modified": "2026-09-24T11:04:12Z",
      "site": {
        "id": "11111111-1111-1111-1111-111111111111",
        "name": "Planta Ejemplo — simulación"
      },
      "zone": null,
      "worker": {
        "id": "33333333-3333-3333-3333-333333333305",
        "external_id": "EMP-004512",
        "name": "Ana Ejemplo",
        "contractor": "Contrata Ejemplo A"
      },
      "device": {
        "id": "44444444-4444-4444-4444-444444444405",
        "serial": "OS-W-0005",
        "battery": 64,
        "date_last_seen": "2026-09-24T11:04:10Z"
      },
      "location": {
        "latitude": 40.00121,
        "longitude": -3.00214,
        "uncertainty_m": 8,
        "source": "gps",
        "masked": false,
        "position_date_utc": "2026-09-24T11:04:08Z",
        "height": null
      },
      "location_withheld": false,
      "details": {
        "trigger": "watch_button"
      },
      "nearby": [
        {
          "kind": "vehicle",
          "worker": {
            "id": "33333333-3333-3333-3333-333333333309",
            "external_id": "EMP-002210",
            "name": "Carlos Ejemplo",
            "contractor": "Contrata Ejemplo B"
          },
          "vehicle_type": "Grúa móvil",
          "distance_m": 18,
          "reported_at": "2026-09-24T11:04:02Z"
        },
        {
          "kind": "person",
          "worker": {
            "id": "33333333-3333-3333-3333-333333333301",
            "external_id": "EMP-000981",
            "name": "Diego Ejemplo",
            "contractor": null
          },
          "vehicle_type": null,
          "distance_m": 26,
          "reported_at": "2026-09-24T11:03:58Z"
        }
      ],
      "vehicle_warnings": [],
      "worker_acknowledged_at": null,
      "acknowledged": null,
      "resolved": null,
      "assigned_to": null,
      "simulated": false,
      "panel_url": "https://app.safeonuba.com/alertas/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
    }
  }
}
```

## Changes to an evacuation

Your destination receives a `POST` for each `muster.*` event. It is signed; it carries no token.

One `POST` per event to each subscribed destination, with the complete object.

Headers: `X-SafeOnuba-Event-Id`, `X-SafeOnuba-Timestamp` (Unix seconds) and
`X-SafeOnuba-Signature: v1=<hex(HMAC_SHA256(secret, timestamp + "." + body))>`. Reject
anything more than 5 min out of sync. During a rotation two signatures arrive, separated by a comma.

Acknowledgement: any `2xx` in under 5 s. Retries at 10 s, 30 s, 2 min, 10 min, 30 min,
1 h and then every hour up to 24 h. “At least once” delivery (deduplicate by `id`) and **no guaranteed
order** (use `date_last_modified`). SOS, falls, “I can’t evacuate” and evacuations go out
before the rest.

### Body (`application/json`, schema `Event`)

- `id` (string (uuid), required): For deduplication: delivery is “at least once”.
- `type` (EventType, required) Values: `alert.created`, `alert.acknowledged`, `alert.resolved`, `alert.reopened`, `alert.worker_acknowledged`, `muster.started`, `muster.kind_changed`, `muster.phase_changed`, `muster.targets_changed`, `muster.updated`, `muster.worker_missing`, `muster.ended`.
- `api_version` (string, required) Values: `2026-09-24`.
- `created_at` (string (date-time), required)
- `organization_id` (string (uuid) | null, required): The site’s company. `null` in test events.
- `site_id` (string (uuid), required)
- `data` ({ alert } | { muster }, required): The complete object exactly as the `GET` would return it at that moment.
  - Variant 1:
    - `alert` (Alert, required)
      - `id` (string (uuid), required)
      - `type` (AlertType, required) Values: `sos`, `fall_detected`, `restricted_zone_entry`, `forbidden_zone_entry`, `permit_expired_inside`, `permit_closed_inside`, `heat_stress`, `evacuation_help`, `worker_call_request`, `device_offline`, `low_battery`, `earthquake`, `tsunami_risk`.
      - `severity` (Severity, required) Values: `low`, `medium`, `high`, `critical`.
      - `status` (AlertStatus, required) Values: `unacknowledged`, `acknowledged`, `resolved`.
      - `title` (string, required): With `include_worker_names`, the title the control room reads (it carries the person’s name). Without it, one composed of the type, the zone and the `external_id`, with no name.
      - `date_created` (string (date-time), required)
      - `date_last_modified` (string (date-time), required): For ordering: webhooks don’t guarantee order, and anything older than what you already have is discarded.
      - `site` (SiteRef, required)
        - `id` (string (uuid), required)
        - `name` (string, required)
      - `zone` (ZoneRef, required)
        - `id` (string (uuid), required)
        - `name` (string, required)
      - `worker` (WorkerRef, required)
        - `id` (string (uuid), required)
        - `external_id` (string | null, required): The worker’s code at the company (the one in its personnel system). `null` if not given.
        - `name` (string): Full name. **Only** if the contract includes it; otherwise the property doesn’t appear.
        - `contractor` (string | null, required)
      - `device` (DeviceRef, required)
        - `id` (string (uuid), required)
        - `serial` (string, required)
        - `battery` (integer | null, required) (from 0 to 100)
        - `date_last_seen` (string (date-time) | null, required)
      - `location` (Location, required)
        - `latitude` (number, required) (from -90 to 90)
        - `longitude` (number, required) (from -180 to 180)
        - `uncertainty_m` (number | null, required): Uncertainty radius in metres. `null` if unknown: not knowing it is not the same as it being perfect. (minimum 0)
        - `source` (string, required): `zone`: the person was in a privacy zone and the point is the zone’s, not theirs. Values: `gps`, `network`, `zone`.
        - `masked` (boolean, required): The point is the representative one for a privacy zone.
        - `position_date_utc` (string (date-time), required)
        - `height` (object | null, required): Height above ground by barometer, when it can be stated; `null` if not.
      - `location_withheld` (boolean, required): The position exists but is not delivered: in “emergencies only”, an alert below the severity that lifts the veil doesn’t show it, and neither does the dashboard.
      - `details` (AlertDetails, required)
        - Variant `SosDetails`:
          - `trigger` (string, required): How it was requested: on the watch screen, with the physical button, after an unanswered fall, or from the control room. Values: `watch_button`, `watch_hardware_button`, `after_fall`, `control_room`.
        - Variant `FallDetails`:
          - `escalated_to_sos` (boolean, required): Did not answer the watch’s “Are you OK?” and it escalated to SOS.
        - Variant `ZoneDetails`:
          - `zone_type` (string, required) Values: `restricted`, `forbidden`.
          - `permit_reference` (string | null, required): The permit to work (job safety analysis, JSA) in force, if there was one.
        - Variant `HeatStressDetails`:
          - `action` (string, required): What the watch has told them: stop, drink and find shade. There are no levels. Values: `stop_and_rest`.
          - `heat_index_c` (number | null, required): The plant’s heat index (°C).
          - `sun_exposure_minutes` (integer | null, required)
        - Variant `EvacuationHelpDetails`:
          - `reason` (string | null, required): What they said on the watch: trapped, injured or helping someone else. Values: `trapped`, `injured`, `helping`.
          - `muster_id` (string (uuid), required)
        - Variant `WorkerCallDetails`:
          - `voice_note` (boolean, required): With no coverage, the watch recorded a voice message (it can be heard in the dashboard).
        - Variant `DeviceOfflineDetails`:
          - `minutes_silent` (integer, required)
        - Variant `LowBatteryDetails`:
          - `battery_pct` (integer, required) (from 0 to 100)
        - Variant `HazardDetails`:
          - `magnitude` (number | null, required)
          - `distance_km` (number | null, required)
          - `official` (boolean, required): `false`: a SafeOnuba estimate with no official warning. `true`: an official bulletin confirmed by a person.
          - `felt_at_site` (boolean, required)
          - `eta` (string (date-time) | null, required): Estimated arrival of the wave (tsunami).
      - `nearby` (array<Nearby>, required)
        - `kind` (string, required) Values: `person`, `vehicle`.
        - `worker` (object, required)
        - `vehicle_type` (string | null, required)
        - `distance_m` (integer, required)
        - `reported_at` (string (date-time), required)
      - `vehicle_warnings` (array<VehicleWarning>, required)
        - `at` (string (date-time), required)
        - `vehicle_type` (string | null, required)
        - `distance_m` (integer, required)
        - `worker_acknowledged_at` (string (date-time) | null, required)
      - `worker_acknowledged_at` (string (date-time) | null, required): The worker pressed “Got it” on their watch (watch time).
      - `acknowledged` (ActionBy, required)
        - `at` (string (date-time), required)
        - `by` (string, required): Who: a person from the dashboard, or “Client · Operator (via integration)”.
      - `resolved` (object | null, required): Resolved: by whom, when and with what note.
        - `at` (string (date-time), required)
        - `by` (string, required): Who: a person from the dashboard, or “Client · Operator (via integration)”.
        - `note` (string | null, required)
      - `assigned_to` (string | null, required)
      - `simulated` (boolean, required): Raised in a drill or with simulated watches.
      - `panel_url` (string (uri), required)
  - Variant 2:
    - `muster` (Muster, required)
      - `id` (string (uuid), required)
      - `site` (SiteRef, required)
        - `id` (string (uuid), required)
        - `name` (string, required)
      - `status` (string, required) Values: `active`, `completed`, `cancelled`.
      - `phase` (string, required): `inspecting`: headcount complete and the control room is checking the plant; watches say “do not go back until further notice”. Values: `evacuating`, `inspecting`.
      - `kind` (MusterKind, required) Values: `general`, `tsunami`, `earthquake`.
      - `reason` (string | null, required)
      - `started_at` (string (date-time), required)
      - `ended_at` (string (date-time) | null, required)
      - `deadline` (object | null, required)
        - `at` (string (date-time), required)
        - `source` (string, required): Tsunami: the time of an official warning, or the ceiling in the zone’s plan. Never a physical calculation. Values: `official`, `plan_estimate`.
      - `origin` (object | null, required): Where the emergency comes from. `null` in an evacuation with no origin (a general drill).
        - `latitude` (number, required)
        - `longitude` (number, required)
        - `radius_m` (integer, required)
        - `label` (string | null, required)
        - `zone` (ZoneRef, required)
      - `targets_revision` (integer, required): Goes up every time the targets change (ruling out, restoring, change of type).
      - `totals` (object, required): The same figures as the dashboard banner and the closing summary: the server counts them. In an ended evacuation, what remains is what was recorded on ending —arrived and total— and the rest comes out as `null`.
        - `expected` (integer, required)
        - `safe` (integer, required)
        - `help` (integer | null, required)
        - `wrong_point` (integer | null, required)
        - `pending` (integer | null, required)
        - `no_signal` (integer | null, required)
        - `not_worn` (integer | null, required)
      - `assembly_points` (array<AssemblyPoint>, required): The targets for the type, ruled-out ones included.
        - `id` (string (uuid), required)
        - `name` (string, required)
        - `status` (string, required) Values: `available`, `excluded`.
        - `count` (integer | null, required): People safe at this point. `null` in an ended evacuation: on ending, only the total is kept.
        - `excluded` (object | null, required)

### Responses

- `200`: Received. Any `2xx` will do.

### Example body

```json
{
  "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "type": "alert.created",
  "api_version": "2026-09-24",
  "created_at": "2026-09-24T11:04:12Z",
  "organization_id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "site_id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "data": {
    "alert": {
      "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
      "type": "sos",
      "severity": "critical",
      "status": "unacknowledged",
      "title": "SOS activado — Ana Ejemplo",
      "date_created": "2026-09-24T11:04:12Z",
      "date_last_modified": "2026-09-24T11:04:12Z",
      "site": {
        "id": "11111111-1111-1111-1111-111111111111",
        "name": "Planta Ejemplo — simulación"
      },
      "zone": null,
      "worker": {
        "id": "33333333-3333-3333-3333-333333333305",
        "external_id": "EMP-004512",
        "name": "Ana Ejemplo",
        "contractor": "Contrata Ejemplo A"
      },
      "device": {
        "id": "44444444-4444-4444-4444-444444444405",
        "serial": "OS-W-0005",
        "battery": 64,
        "date_last_seen": "2026-09-24T11:04:10Z"
      },
      "location": {
        "latitude": 40.00121,
        "longitude": -3.00214,
        "uncertainty_m": 8,
        "source": "gps",
        "masked": false,
        "position_date_utc": "2026-09-24T11:04:08Z",
        "height": null
      },
      "location_withheld": false,
      "details": {
        "trigger": "watch_button"
      },
      "nearby": [
        {
          "kind": "vehicle",
          "worker": {
            "id": "33333333-3333-3333-3333-333333333309",
            "external_id": "EMP-002210",
            "name": "Carlos Ejemplo",
            "contractor": "Contrata Ejemplo B"
          },
          "vehicle_type": "Grúa móvil",
          "distance_m": 18,
          "reported_at": "2026-09-24T11:04:02Z"
        },
        {
          "kind": "person",
          "worker": {
            "id": "33333333-3333-3333-3333-333333333301",
            "external_id": "EMP-000981",
            "name": "Diego Ejemplo",
            "contractor": null
          },
          "vehicle_type": null,
          "distance_m": 26,
          "reported_at": "2026-09-24T11:03:58Z"
        }
      ],
      "vehicle_warnings": [],
      "worker_acknowledged_at": null,
      "acknowledged": null,
      "resolved": null,
      "assigned_to": null,
      "simulated": false,
      "panel_url": "https://app.safeonuba.com/alertas/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
    }
  }
}
```
