v1 · preview

Try webhook signature, alert.created, location_withheld or 429.

↑ ↓ moveEnter openEsc close

Real time

Events

Event types, the common envelope and how to recover missed events with /v1/events, which returns exactly what was sent by webhook.

On this page

An event is a change: an alert being created, an operator acknowledging it, a headcount going up. The same events arrive by webhook and can be requested again with GET /v1/events.

Event types

typeWhat it means
alert.createdAn alert has been created.
alert.acknowledgedAn operator has taken charge of the alert, from the dashboard or from your control room.
alert.resolvedThe alert has been closed.
alert.reopenedA closed alert has been reopened.
alert.worker_acknowledgedThe worker has seen the alert on their watch (worker_acknowledged_at).
muster.startedAn evacuation has started.
muster.kind_changedThe evacuation type has changed (general, tsunami, earthquake).
muster.phase_changedThe phase has changed: evacuating or inspecting.
muster.targets_changedThe control room has ruled out or restored a muster point.
muster.updatedThe headcount has changed. It is checked every 5 seconds and only emitted when it changes.
muster.worker_missingSomeone is missing.
muster.endedThe evacuation has ended.

When you create a destination you can subscribe to whole groups (alert.*, muster.*) or to specific types.

The envelope

Every event has the same shape:

FieldWhat it is
idEvent identifier, for deduplication.
typeOne of the types above.
api_version"2026-09-24".
created_atWhen it happened.
organization_idThe site’s company. null in test events.
site_idThe site.
data{ "alert": … } in alert.* events and { "muster": … } in muster.* events.

What goes inside data is the complete object exactly as the GET would return it at that moment: the whole alert, or the evacuation with its headcount and its muster points. It isn’t a “something changed, come and look” notice; it’s the state.

alert.created
{
  "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"
    }
  }
}

The full schema is in the reference: Event.

Catching up on what you missed

If your receiver has been down, or you want to check you aren’t missing anything, GET /v1/events returns the events from the last 30 days in publication order, oldest first. They are exactly the same bytes that were sent by webhook, so you process them with the same code.

ParameterWhat it is
sinceRequired. From when, at most 30 days back.
typeA single event type only.
limit, cursorPagination (see Pagination).

Any scope will do.

// Catches up from the last event you processed.
async function catchUp(since) {
  let cursor = null;
  do {
    const url = new URL('https://sandbox.api.safeonuba.com/v1/events');
    url.searchParams.set('since', since);
    url.searchParams.set('limit', '500');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${await safeOnubaToken()}` } });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const page = await res.json();

    for (const event of page.data) await handleEvent(event); // the same code as the webhook; deduplicate by id
    cursor = page.next_cursor;
  } while (cursor);
}

When to use it

  • When your receiver starts up after downtime, from the created_at of the last event you processed.
  • After a failed-delivery notice: if a delivery runs out of retries, we email your technical contact. The event is still in /v1/events.
  • To fill gaps if you notice you’re missing an intermediate step.

Since it’s the same “at least once” delivery, deduplicate by id: most likely some of what you recover you had already received.