Tiempo real
Eventos
Los tipos de evento, el sobre común y cómo recuperar los que se perdieron con /v1/events, que devuelve exactamente lo mismo que se envió por webhook.
En esta página
Un evento es un cambio: una alerta que se crea, un operador que la reconoce, un recuento que sube. Los mismos eventos llegan por webhook y se pueden volver a pedir con GET /v1/events.
Tipos de evento
| type | Qué significa |
|---|---|
alert.created | Se ha creado una alerta. |
alert.acknowledged | Un operador se ha hecho cargo de la alerta, desde el panel o desde tu sala. |
alert.resolved | La alerta se ha cerrado. |
alert.reopened | Una alerta cerrada se ha vuelto a abrir. |
alert.worker_acknowledged | El trabajador ha visto el aviso en su reloj (worker_acknowledged_at). |
muster.started | Ha empezado una evacuación. |
muster.kind_changed | Ha cambiado el tipo de la evacuación (general, tsunami, terremoto). |
muster.phase_changed | Ha cambiado la fase: evacuating o inspecting. |
muster.targets_changed | La sala ha descartado o rehabilitado un punto de reunión. |
muster.updated | El recuento ha cambiado. Se comprueba cada 5 segundos y solo se emite si cambia. |
muster.worker_missing | Falta una persona. |
muster.ended | La evacuación ha terminado. |
Al crear un destino puedes suscribirte a grupos enteros (alert.*, muster.*) o a tipos concretos.
El sobre
Todos los eventos tienen la misma forma:
| Campo | Qué es |
|---|---|
id | Identificador del evento, para deduplicar. |
type | Uno de los tipos de arriba. |
api_version | "2026-09-24". |
created_at | Cuándo ocurrió. |
organization_id | La empresa del centro. null en los eventos de prueba. |
site_id | El centro. |
data | { "alert": … } en los alert.* y { "muster": … } en los muster.*. |
Lo que va dentro de data es el objeto completo tal como lo devolvería el GET en ese instante: la alerta entera, o la evacuación con su recuento y sus puntos de reunión. No es un aviso de «algo ha cambiado, ven a mirarlo»; es el estado.
{
"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"
}
}
}El esquema completo está en la referencia: Event.
Recuperar lo perdido
Si tu receptor ha estado caído, o quieres comprobar que no te falta nada, GET /v1/events devuelve los eventos de los últimos 30 días en orden de publicación, del más antiguo al más reciente. Son exactamente los mismos bytes que se enviaron por webhook, así que los procesas con el mismo código.
| Parámetro | Qué es |
|---|---|
since | Obligatorio. Desde cuándo, como mucho 30 días atrás. |
type | Solo un tipo de evento. |
limit, cursor | Paginación (ver Paginación). |
Vale con cualquier scope.
// Se pone al día desde el último evento procesado.
async function recuperar(desde) {
let cursor = null;
do {
const url = new URL('https://sandbox.api.safeonuba.com/v1/events');
url.searchParams.set('since', desde);
url.searchParams.set('limit', '500');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${await tokenSafeOnuba()}` } });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const pagina = await res.json();
for (const evento of pagina.data) await procesar(evento); // el mismo código que el webhook; deduplica por id
cursor = pagina.next_cursor;
} while (cursor);
}def recuperar(desde: str) -> None:
"""Se pone al día desde el último evento procesado."""
cursor = None
while True:
params = {"since": desde, "limit": 500}
if cursor:
params["cursor"] = cursor
res = requests.get(
"https://sandbox.api.safeonuba.com/v1/events",
headers={"Authorization": f"Bearer {token_safeonuba()}"},
params=params,
timeout=30,
)
res.raise_for_status()
pagina = res.json()
for evento in pagina["data"]:
procesar(evento) # el mismo código que el webhook; deduplica por id
cursor = pagina["next_cursor"]
if not cursor:
breakasync Task Recuperar(string desde)
{
string? cursor = null;
do
{
var url = $"https://sandbox.api.safeonuba.com/v1/events?since={Uri.EscapeDataString(desde)}&limit=500"
+ (cursor is null ? "" : $"&cursor={Uri.EscapeDataString(cursor)}");
var res = await http.GetAsync(url);
res.EnsureSuccessStatusCode();
var pagina = await res.Content.ReadFromJsonAsync<JsonElement>();
foreach (var evento in pagina.GetProperty("data").EnumerateArray())
await Procesar(evento); // el mismo código que el webhook; deduplica por id
var siguiente = pagina.GetProperty("next_cursor");
cursor = siguiente.ValueKind == JsonValueKind.Null ? null : siguiente.GetString();
} while (cursor is not null);
}Cuándo usarlo
- Al arrancar tu receptor tras una parada, desde el
created_atdel último evento que procesaste. - Tras un aviso de entrega perdida: si un envío agota sus reintentos, avisamos por correo a tu contacto técnico. El evento sigue en
/v1/events. - Para rellenar huecos si ves que te falta un paso intermedio.
Como es la misma entrega «al menos una vez», deduplica por id: lo más normal es que parte de lo que recuperes ya lo hubieras recibido.