Referencia
Webhooks
Destinos y el formato de lo que se les envía.
Listar destinos
GET/v1/webhooks
Scope webhooks:manage
Respuestas
- 200
Destinos.
application/json
WebhookEndpointList - 401
Falta el token, ha caducado o se ha revocado.
application/problem+json
Problem - 403
Al token le falta el scope que pide la ruta, o el centro no es del cliente.
application/problem+json
Problem - 429
Demasiadas peticiones, o demasiado tiempo de consulta: cada cliente tiene un número de peticiones por minuto (600 de serie) y un tiempo de consulta por minuto (30 s de serie), y cada petición gasta lo que tarda, así que una lista larga cuenta más que una alerta suelta.
Retry-Afterdice cuándo volver.application/problem+json
Problem
Petición · sandbox
curl "https://sandbox.api.safeonuba.com/v1/webhooks" \
-H "Authorization: Bearer $SAFEONUBA_TOKEN"const res = await fetch('https://sandbox.api.safeonuba.com/v1/webhooks', {
headers: {
Authorization: `Bearer ${process.env.SAFEONUBA_TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const data = await res.json();import os
import requests
res = requests.get(
"https://sandbox.api.safeonuba.com/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['SAFEONUBA_TOKEN']}"},
timeout=10,
)
res.raise_for_status()
data = res.json()using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("SAFEONUBA_TOKEN"));
var res = await http.GetAsync("https://sandbox.api.safeonuba.com/v1/webhooks");
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();Respuesta 200
{
"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"
}
]
}Crear un destino
POST/v1/webhooks
Scope webhooks:manage
Cuerpo application/json WebhookEndpointCreate
urlstringobligatorioevent_typesarray<EventType | "alert.*" | "muster.*">obligatoriofiltersobjectCampos3
site_idsarray<string (uuid)>min_severitySeverityValores posibles:
lowmediumhighcriticalcontractorsarray<string>
Respuestas
- 201
Destino creado, con su secreto de firma (solo esta vez).
application/json
WebhookEndpointCreated - 401
Falta el token, ha caducado o se ha revocado.
application/problem+json
Problem - 403
Al token le falta el scope que pide la ruta, o el centro no es del cliente.
application/problem+json
Problem - 422
El cuerpo no es válido (la URL tiene que ser https).
application/problem+json
Problem - 429
Demasiadas peticiones, o demasiado tiempo de consulta: cada cliente tiene un número de peticiones por minuto (600 de serie) y un tiempo de consulta por minuto (30 s de serie), y cada petición gasta lo que tarda, así que una lista larga cuenta más que una alerta suelta.
Retry-Afterdice cuándo volver.application/problem+json
Problem
Petición · sandbox
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.*"
]
}'const res = await fetch('https://sandbox.api.safeonuba.com/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SAFEONUBA_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://sala.cliente.example/safeonuba",
"event_types": [
"alert.*",
"muster.*"
]
}),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const data = await res.json();import os
import requests
res = requests.post(
"https://sandbox.api.safeonuba.com/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['SAFEONUBA_TOKEN']}"},
json={
"url": "https://sala.cliente.example/safeonuba",
"event_types": ["alert.*", "muster.*"],
},
timeout=10,
)
res.raise_for_status()
data = res.json()using System.Net.Http.Headers;
using System.Text;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("SAFEONUBA_TOKEN"));
var contenido = new StringContent("""
{
"url": "https://sala.cliente.example/safeonuba",
"event_types": [
"alert.*",
"muster.*"
]
}
""", Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://sandbox.api.safeonuba.com/v1/webhooks", contenido);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();Respuesta 201
{
"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"
}Borrar un destino
DEL/v1/webhooks/{id}
Scope webhooks:manage
Parámetros de ruta
idstring (uuid)obligatorioId del destino.
Respuestas
- 204
Borrado.
- 401
Falta el token, ha caducado o se ha revocado.
application/problem+json
Problem - 403
Al token le falta el scope que pide la ruta, o el centro no es del cliente.
application/problem+json
Problem - 404
No existe.
application/problem+json
Problem - 429
Demasiadas peticiones, o demasiado tiempo de consulta: cada cliente tiene un número de peticiones por minuto (600 de serie) y un tiempo de consulta por minuto (30 s de serie), y cada petición gasta lo que tarda, así que una lista larga cuenta más que una alerta suelta.
Retry-Afterdice cuándo volver.application/problem+json
Problem
Petición · sandbox
curl -X DELETE "https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90" \
-H "Authorization: Bearer $SAFEONUBA_TOKEN"const res = await fetch('https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.SAFEONUBA_TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);import os
import requests
res = requests.delete(
"https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
headers={"Authorization": f"Bearer {os.environ['SAFEONUBA_TOKEN']}"},
timeout=10,
)
res.raise_for_status()using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("SAFEONUBA_TOKEN"));
var res = await http.DeleteAsync("https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90");
res.EnsureSuccessStatusCode();Respuesta 204
HTTP 204Rotar el secreto de firma
POST/v1/webhooks/{id}/rotate-secret
Scope webhooks:manage
Da un secreto nuevo. El anterior sigue firmando 24 h: durante ese tiempo cada envío lleva las dos firmas en X-SafeOnuba-Signature, separadas por coma, así que el receptor puede cambiar de secreto sin cortar. Una segunda rotación retira el primero: nunca hay más de dos.
Parámetros de ruta
idstring (uuid)obligatorioId del destino.
Respuestas
- 200
El secreto nuevo (solo esta vez) y hasta cuándo firma el anterior.
application/json
WebhookSecretRotated - 401
Falta el token, ha caducado o se ha revocado.
application/problem+json
Problem - 403
Al token le falta el scope que pide la ruta, o el centro no es del cliente.
application/problem+json
Problem - 404
No existe.
application/problem+json
Problem - 429
Demasiadas peticiones, o demasiado tiempo de consulta: cada cliente tiene un número de peticiones por minuto (600 de serie) y un tiempo de consulta por minuto (30 s de serie), y cada petición gasta lo que tarda, así que una lista larga cuenta más que una alerta suelta.
Retry-Afterdice cuándo volver.application/problem+json
Problem
Petición · sandbox
curl -X POST "https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/rotate-secret" \
-H "Authorization: Bearer $SAFEONUBA_TOKEN"const res = await fetch('https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/rotate-secret', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SAFEONUBA_TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const data = await res.json();import os
import requests
res = requests.post(
"https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/rotate-secret",
headers={"Authorization": f"Bearer {os.environ['SAFEONUBA_TOKEN']}"},
timeout=10,
)
res.raise_for_status()
data = res.json()using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("SAFEONUBA_TOKEN"));
var res = await http.PostAsync("https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/rotate-secret", null);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();Respuesta 200
{
"secret": "whsec_6f1c0d9e2b7a4c3f8e5d1a0b9c8e7f6a",
"previous_valid_until": "2026-09-24T11:04:12Z"
}Enviar un evento de prueba
POST/v1/webhooks/{id}/test
Scope webhooks:manage
Manda un alert.created marcado simulated: true, firmado como uno real.
Parámetros de ruta
idstring (uuid)obligatorioId del destino.
Respuestas
- 202
En cola: el resultado se ve en el destino (
failing_since). - 401
Falta el token, ha caducado o se ha revocado.
application/problem+json
Problem - 403
Al token le falta el scope que pide la ruta, o el centro no es del cliente.
application/problem+json
Problem - 404
No existe.
application/problem+json
Problem - 429
Demasiadas peticiones, o demasiado tiempo de consulta: cada cliente tiene un número de peticiones por minuto (600 de serie) y un tiempo de consulta por minuto (30 s de serie), y cada petición gasta lo que tarda, así que una lista larga cuenta más que una alerta suelta.
Retry-Afterdice cuándo volver.application/problem+json
Problem
Petición · sandbox
curl -X POST "https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/test" \
-H "Authorization: Bearer $SAFEONUBA_TOKEN"const res = await fetch('https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SAFEONUBA_TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);import os
import requests
res = requests.post(
"https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/test",
headers={"Authorization": f"Bearer {os.environ['SAFEONUBA_TOKEN']}"},
timeout=10,
)
res.raise_for_status()using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("SAFEONUBA_TOKEN"));
var res = await http.PostAsync("https://sandbox.api.safeonuba.com/v1/webhooks/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90/test", null);
res.EnsureSuccessStatusCode();Respuesta 202
HTTP 202Lo que recibe tu destino
Cada evento llega a tu URL como un POST con este cuerpo. Los dos grupos comparten formato: cambia lo que va dentro de data.
Cambios en una alerta
POSTTu destino · alert.*
Lo recibe tu destino; va firmado, no lleva token.
Un POST por evento a cada destino suscrito, con el objeto completo.
Cabeceras: X-SafeOnuba-Event-Id, X-SafeOnuba-Timestamp (segundos Unix) y X-SafeOnuba-Signature: v1=<hex(HMAC_SHA256(secreto, timestamp + "." + cuerpo))>. Rechaza más de 5 min de desfase. Durante una rotación llegan dos firmas separadas por coma.
Acuse: cualquier 2xx en menos de 5 s. Reintentos a los 10 s, 30 s, 2 min, 10 min, 30 min, 1 h y cada hora hasta 24 h. Entrega «al menos una vez» (deduplica por id) y sin orden garantizado (usa date_last_modified). SOS, caídas, «no puedo evacuar» y evacuaciones salen antes que el resto.
Cuerpo application/json Event
idstring (uuid)obligatorioPara deduplicar: la entrega es «al menos una vez».
Valores posibles:
alert.createdalert.acknowledgedalert.resolvedalert.reopenedalert.worker_acknowledgedmuster.startedmuster.kind_changedmuster.phase_changedmuster.targets_changedmuster.updatedmuster.worker_missingmuster.endedapi_versionstringobligatorioValores posibles:
2026-09-24created_atstring (date-time)obligatorioorganization_idstring (uuid) | nullobligatorioLa empresa del centro.
nullen los eventos de prueba.site_idstring (uuid)obligatoriodata{ alert } | { muster }obligatorioEl objeto completo tal como lo devolvería el
GETen ese instante.Una de 2 formas
- variante 1
Campos de Alert22
idstring (uuid)obligatorioValores posibles:
sosfall_detectedrestricted_zone_entryforbidden_zone_entrypermit_expired_insidepermit_closed_insideheat_stressevacuation_helpworker_call_requestdevice_offlinelow_batteryearthquaketsunami_riskValores posibles:
lowmediumhighcriticalValores posibles:
unacknowledgedacknowledgedresolvedtitlestringobligatorioCon
include_worker_names, el título que lee la sala (lleva el nombre de la persona). Sin él, uno compuesto con el tipo, la zona y elexternal_id, sin nombre.date_createdstring (date-time)obligatoriodate_last_modifiedstring (date-time)obligatorioPara ordenar: los webhooks no garantizan el orden, y lo más viejo que lo que ya se tiene se descarta.
Campos de SiteRef2
idstring (uuid)obligatorionamestringobligatorio
Campos de ZoneRef2
idstring (uuid)obligatorionamestringobligatorio
Campos de WorkerRef4
idstring (uuid)obligatorioexternal_idstring | nullobligatorioCódigo del trabajador en la empresa (el de su sistema de personal).
nullsi no se ha dado.namestringNombre y apellidos. Solo si el contrato lo incluye; si no, la propiedad no aparece.
contractorstring | nullobligatorio
Campos de DeviceRef4
idstring (uuid)obligatorioserialstringobligatoriobatteryinteger | nullobligatoriode 0 a 100
date_last_seenstring (date-time) | nullobligatorio
Campos de Location7
latitudenumberobligatoriode -90 a 90
longitudenumberobligatoriode -180 a 180
uncertainty_mnumber | nullobligatorioRadio de incertidumbre en metros.
nullsi no se conoce: no saberla no es tenerla perfecta.mínimo 0
sourcestringobligatoriozone: la persona estaba en una zona de privacidad y el punto es el de la zona, no el suyo.Valores posibles:
gpsnetworkzonemaskedbooleanobligatorioEl punto es el representativo de una zona de privacidad.
position_date_utcstring (date-time)obligatorioheightobject | nullobligatorioAltura sobre el suelo por barómetro, cuando se puede afirmar;
nullsi no.
location_withheldbooleanobligatorioLa posición existe pero no se entrega: en «Solo en emergencia», una alerta por debajo de la gravedad que destapa no la enseña, tampoco en el panel.
Una de 9 formas
triggerstringobligatorioCómo se pidió: en la pantalla del reloj, con el botón físico, tras una caída sin respuesta o desde la sala.
Valores posibles:
watch_buttonwatch_hardware_buttonafter_fallcontrol_room
escalated_to_sosbooleanobligatorioNo contestó al «¿Estás bien?» del reloj y pasó a SOS.
zone_typestringobligatorioValores posibles:
restrictedforbiddenpermit_referencestring | nullobligatorioEl permiso de trabajo (ATS) que regía, si lo había.
Sin nada que salga del pulso: las constantes vitales no salen por la API, ni agregadas.
actionstringobligatorioLo que se le ha dicho en el reloj: parar, beber y buscar sombra. No hay niveles.
Valores posibles:
stop_and_restheat_index_cnumber | nullobligatorioÍndice de calor de la planta (°C).
sun_exposure_minutesinteger | nullobligatorio
reasonstring | nullobligatorioLo que dijo en el reloj: atrapado, herido o ayudando a otra persona.
Valores posibles:
trappedinjuredhelpingmuster_idstring (uuid)obligatorio
voice_notebooleanobligatorioSin cobertura, el reloj grabó un aviso de voz (se escucha en el panel).
minutes_silentintegerobligatorio
battery_pctintegerobligatoriode 0 a 100
magnitudenumber | nullobligatoriodistance_kmnumber | nullobligatorioofficialbooleanobligatoriofalse: estimación de SafeOnuba sin aviso oficial.true: boletín oficial confirmado por una persona.felt_at_sitebooleanobligatorioetastring (date-time) | nullobligatorioLlegada estimada de la ola (tsunami).
Campos de Nearby5
kindstringobligatorioValores posibles:
personvehicleworkerobjectobligatoriovehicle_typestring | nullobligatoriodistance_mintegerobligatorioreported_atstring (date-time)obligatorio
Campos de VehicleWarning4
atstring (date-time)obligatoriovehicle_typestring | nullobligatoriodistance_mintegerobligatorioworker_acknowledged_atstring (date-time) | nullobligatorio
worker_acknowledged_atstring (date-time) | nullobligatorioEl trabajador pulsó «Entendido» en su reloj (hora del reloj).
Campos de ActionBy2
atstring (date-time)obligatoriobystringobligatorioQuién: una persona del panel, o «Cliente · Operador (vía integración)».
resolvedobject | nullobligatorioCerrada: quién, cuándo y con qué nota.
Campos3
atstring (date-time)obligatoriobystringobligatorioQuién: una persona del panel, o «Cliente · Operador (vía integración)».
notestring | nullobligatorio
assigned_tostring | nullobligatoriosimulatedbooleanobligatorioGenerada en un simulacro o con relojes simulados.
panel_urlstring (uri)obligatorio
- variante 2
Campos de Muster13
idstring (uuid)obligatorioCampos de SiteRef2
idstring (uuid)obligatorionamestringobligatorio
statusstringobligatorioValores posibles:
activecompletedcancelledphasestringobligatorioinspecting: recuento completo y la sala revisa la planta; los relojes dicen «no vuelvas hasta nuevo aviso».Valores posibles:
evacuatinginspectingValores posibles:
generaltsunamiearthquakereasonstring | nullobligatoriostarted_atstring (date-time)obligatorioended_atstring (date-time) | nullobligatoriodeadlineobject | nullobligatorioCampos2
atstring (date-time)obligatoriosourcestringobligatorioTsunami: hora de un aviso oficial, o el techo del plan de la zona. Nunca un cálculo físico.
Valores posibles:
officialplan_estimate
originobject | nullobligatorioDe dónde viene la emergencia.
nullen una evacuación sin origen (un simulacro general).Campos5
latitudenumberobligatoriolongitudenumberobligatorioradius_mintegerobligatoriolabelstring | nullobligatorio
targets_revisionintegerobligatorioSube cada vez que cambian los destinos (descarte, rehabilitación, cambio de tipo).
totalsobjectobligatorioLas mismas cifras que el banner del panel y el cierre: las cuenta el servidor. En una evacuación cerrada queda lo que se registró al cerrar —llegados y total—, y el resto sale
null.Campos7
expectedintegerobligatoriosafeintegerobligatoriohelpinteger | nullobligatoriowrong_pointinteger | nullobligatoriopendinginteger | nullobligatoriono_signalinteger | nullobligatorionot_worninteger | nullobligatorio
Los destinos del tipo, descartados incluidos.
Campos de AssemblyPoint5
idstring (uuid)obligatorionamestringobligatoriostatusstringobligatorioValores posibles:
availableexcludedcountinteger | nullobligatorioPersonas a salvo en este punto.
nullen una evacuación cerrada: al cerrar solo se guarda el total.excludedobject | nullobligatorio
Respuestas
- 200
Recibido. Cualquier
2xxvale.
Cuerpo que recibe tu destino
{
"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"
}
}
}Cómo comprobar que viene de SafeOnuba: verificar la firma.
Cambios en una evacuación
POSTTu destino · muster.*
Lo recibe tu destino; va firmado, no lleva token.
Un POST por evento a cada destino suscrito, con el objeto completo.
Cabeceras: X-SafeOnuba-Event-Id, X-SafeOnuba-Timestamp (segundos Unix) y X-SafeOnuba-Signature: v1=<hex(HMAC_SHA256(secreto, timestamp + "." + cuerpo))>. Rechaza más de 5 min de desfase. Durante una rotación llegan dos firmas separadas por coma.
Acuse: cualquier 2xx en menos de 5 s. Reintentos a los 10 s, 30 s, 2 min, 10 min, 30 min, 1 h y cada hora hasta 24 h. Entrega «al menos una vez» (deduplica por id) y sin orden garantizado (usa date_last_modified). SOS, caídas, «no puedo evacuar» y evacuaciones salen antes que el resto.
Cuerpo application/json Event
idstring (uuid)obligatorioPara deduplicar: la entrega es «al menos una vez».
Valores posibles:
alert.createdalert.acknowledgedalert.resolvedalert.reopenedalert.worker_acknowledgedmuster.startedmuster.kind_changedmuster.phase_changedmuster.targets_changedmuster.updatedmuster.worker_missingmuster.endedapi_versionstringobligatorioValores posibles:
2026-09-24created_atstring (date-time)obligatorioorganization_idstring (uuid) | nullobligatorioLa empresa del centro.
nullen los eventos de prueba.site_idstring (uuid)obligatoriodata{ alert } | { muster }obligatorioEl objeto completo tal como lo devolvería el
GETen ese instante.Una de 2 formas
- variante 1
Campos de Alert22
idstring (uuid)obligatorioValores posibles:
sosfall_detectedrestricted_zone_entryforbidden_zone_entrypermit_expired_insidepermit_closed_insideheat_stressevacuation_helpworker_call_requestdevice_offlinelow_batteryearthquaketsunami_riskValores posibles:
lowmediumhighcriticalValores posibles:
unacknowledgedacknowledgedresolvedtitlestringobligatorioCon
include_worker_names, el título que lee la sala (lleva el nombre de la persona). Sin él, uno compuesto con el tipo, la zona y elexternal_id, sin nombre.date_createdstring (date-time)obligatoriodate_last_modifiedstring (date-time)obligatorioPara ordenar: los webhooks no garantizan el orden, y lo más viejo que lo que ya se tiene se descarta.
Campos de SiteRef2
idstring (uuid)obligatorionamestringobligatorio
Campos de ZoneRef2
idstring (uuid)obligatorionamestringobligatorio
Campos de WorkerRef4
idstring (uuid)obligatorioexternal_idstring | nullobligatorioCódigo del trabajador en la empresa (el de su sistema de personal).
nullsi no se ha dado.namestringNombre y apellidos. Solo si el contrato lo incluye; si no, la propiedad no aparece.
contractorstring | nullobligatorio
Campos de DeviceRef4
idstring (uuid)obligatorioserialstringobligatoriobatteryinteger | nullobligatoriode 0 a 100
date_last_seenstring (date-time) | nullobligatorio
Campos de Location7
latitudenumberobligatoriode -90 a 90
longitudenumberobligatoriode -180 a 180
uncertainty_mnumber | nullobligatorioRadio de incertidumbre en metros.
nullsi no se conoce: no saberla no es tenerla perfecta.mínimo 0
sourcestringobligatoriozone: la persona estaba en una zona de privacidad y el punto es el de la zona, no el suyo.Valores posibles:
gpsnetworkzonemaskedbooleanobligatorioEl punto es el representativo de una zona de privacidad.
position_date_utcstring (date-time)obligatorioheightobject | nullobligatorioAltura sobre el suelo por barómetro, cuando se puede afirmar;
nullsi no.
location_withheldbooleanobligatorioLa posición existe pero no se entrega: en «Solo en emergencia», una alerta por debajo de la gravedad que destapa no la enseña, tampoco en el panel.
Una de 9 formas
triggerstringobligatorioCómo se pidió: en la pantalla del reloj, con el botón físico, tras una caída sin respuesta o desde la sala.
Valores posibles:
watch_buttonwatch_hardware_buttonafter_fallcontrol_room
escalated_to_sosbooleanobligatorioNo contestó al «¿Estás bien?» del reloj y pasó a SOS.
zone_typestringobligatorioValores posibles:
restrictedforbiddenpermit_referencestring | nullobligatorioEl permiso de trabajo (ATS) que regía, si lo había.
Sin nada que salga del pulso: las constantes vitales no salen por la API, ni agregadas.
actionstringobligatorioLo que se le ha dicho en el reloj: parar, beber y buscar sombra. No hay niveles.
Valores posibles:
stop_and_restheat_index_cnumber | nullobligatorioÍndice de calor de la planta (°C).
sun_exposure_minutesinteger | nullobligatorio
reasonstring | nullobligatorioLo que dijo en el reloj: atrapado, herido o ayudando a otra persona.
Valores posibles:
trappedinjuredhelpingmuster_idstring (uuid)obligatorio
voice_notebooleanobligatorioSin cobertura, el reloj grabó un aviso de voz (se escucha en el panel).
minutes_silentintegerobligatorio
battery_pctintegerobligatoriode 0 a 100
magnitudenumber | nullobligatoriodistance_kmnumber | nullobligatorioofficialbooleanobligatoriofalse: estimación de SafeOnuba sin aviso oficial.true: boletín oficial confirmado por una persona.felt_at_sitebooleanobligatorioetastring (date-time) | nullobligatorioLlegada estimada de la ola (tsunami).
Campos de Nearby5
kindstringobligatorioValores posibles:
personvehicleworkerobjectobligatoriovehicle_typestring | nullobligatoriodistance_mintegerobligatorioreported_atstring (date-time)obligatorio
Campos de VehicleWarning4
atstring (date-time)obligatoriovehicle_typestring | nullobligatoriodistance_mintegerobligatorioworker_acknowledged_atstring (date-time) | nullobligatorio
worker_acknowledged_atstring (date-time) | nullobligatorioEl trabajador pulsó «Entendido» en su reloj (hora del reloj).
Campos de ActionBy2
atstring (date-time)obligatoriobystringobligatorioQuién: una persona del panel, o «Cliente · Operador (vía integración)».
resolvedobject | nullobligatorioCerrada: quién, cuándo y con qué nota.
Campos3
atstring (date-time)obligatoriobystringobligatorioQuién: una persona del panel, o «Cliente · Operador (vía integración)».
notestring | nullobligatorio
assigned_tostring | nullobligatoriosimulatedbooleanobligatorioGenerada en un simulacro o con relojes simulados.
panel_urlstring (uri)obligatorio
- variante 2
Campos de Muster13
idstring (uuid)obligatorioCampos de SiteRef2
idstring (uuid)obligatorionamestringobligatorio
statusstringobligatorioValores posibles:
activecompletedcancelledphasestringobligatorioinspecting: recuento completo y la sala revisa la planta; los relojes dicen «no vuelvas hasta nuevo aviso».Valores posibles:
evacuatinginspectingValores posibles:
generaltsunamiearthquakereasonstring | nullobligatoriostarted_atstring (date-time)obligatorioended_atstring (date-time) | nullobligatoriodeadlineobject | nullobligatorioCampos2
atstring (date-time)obligatoriosourcestringobligatorioTsunami: hora de un aviso oficial, o el techo del plan de la zona. Nunca un cálculo físico.
Valores posibles:
officialplan_estimate
originobject | nullobligatorioDe dónde viene la emergencia.
nullen una evacuación sin origen (un simulacro general).Campos5
latitudenumberobligatoriolongitudenumberobligatorioradius_mintegerobligatoriolabelstring | nullobligatorio
targets_revisionintegerobligatorioSube cada vez que cambian los destinos (descarte, rehabilitación, cambio de tipo).
totalsobjectobligatorioLas mismas cifras que el banner del panel y el cierre: las cuenta el servidor. En una evacuación cerrada queda lo que se registró al cerrar —llegados y total—, y el resto sale
null.Campos7
expectedintegerobligatoriosafeintegerobligatoriohelpinteger | nullobligatoriowrong_pointinteger | nullobligatoriopendinginteger | nullobligatoriono_signalinteger | nullobligatorionot_worninteger | nullobligatorio
Los destinos del tipo, descartados incluidos.
Campos de AssemblyPoint5
idstring (uuid)obligatorionamestringobligatoriostatusstringobligatorioValores posibles:
availableexcludedcountinteger | nullobligatorioPersonas a salvo en este punto.
nullen una evacuación cerrada: al cerrar solo se guarda el total.excludedobject | nullobligatorio
Respuestas
- 200
Recibido. Cualquier
2xxvale.
Cuerpo que recibe tu destino
{
"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"
}
}
}Cómo comprobar que viene de SafeOnuba: verificar la firma.