Reference
Webhooks
Destinations and the format of what is sent to them.
List destinations
GET/v1/webhooks
Scope webhooks:manage
Responses
- 200
Destinations.
application/json
WebhookEndpointList - 401
The token is missing, has expired or has been revoked.
application/problem+json
Problem - 403
The token lacks the scope the route requires, or the site doesn’t belong to the client.
application/problem+json
Problem - 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-Aftersays when to retry.application/problem+json
Problem
Request · 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();Response 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"
}
]
}Create a destination
POST/v1/webhooks
Scope webhooks:manage
Body application/json WebhookEndpointCreate
urlstringrequiredevent_typesarray<EventType | "alert.*" | "muster.*">requiredfiltersobjectFields3
site_idsarray<string (uuid)>min_severitySeverityPossible values:
lowmediumhighcriticalcontractorsarray<string>
Responses
- 201
Destination created, with its signing secret (this once only).
application/json
WebhookEndpointCreated - 401
The token is missing, has expired or has been revoked.
application/problem+json
Problem - 403
The token lacks the scope the route requires, or the site doesn’t belong to the client.
application/problem+json
Problem - 422
The body is not valid (the URL must be https).
application/problem+json
Problem - 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-Aftersays when to retry.application/problem+json
Problem
Request · 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();Response 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"
}Delete a destination
DEL/v1/webhooks/{id}
Scope webhooks:manage
Path parameters
idstring (uuid)requiredDestination id.
Responses
- 204
Deleted.
- 401
The token is missing, has expired or has been revoked.
application/problem+json
Problem - 403
The token lacks the scope the route requires, or the site doesn’t belong to the client.
application/problem+json
Problem - 404
It doesn’t exist.
application/problem+json
Problem - 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-Aftersays when to retry.application/problem+json
Problem
Request · 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();Response 204
HTTP 204Rotate the signing secret
POST/v1/webhooks/{id}/rotate-secret
Scope webhooks:manage
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
idstring (uuid)requiredDestination id.
Responses
- 200
The new secret (this once only) and until when the previous one signs.
application/json
WebhookSecretRotated - 401
The token is missing, has expired or has been revoked.
application/problem+json
Problem - 403
The token lacks the scope the route requires, or the site doesn’t belong to the client.
application/problem+json
Problem - 404
It doesn’t exist.
application/problem+json
Problem - 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-Aftersays when to retry.application/problem+json
Problem
Request · 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();Response 200
{
"secret": "whsec_6f1c0d9e2b7a4c3f8e5d1a0b9c8e7f6a",
"previous_valid_until": "2026-09-24T11:04:12Z"
}Send a test event
POST/v1/webhooks/{id}/test
Scope webhooks:manage
Sends an alert.created marked simulated: true, signed like a real one.
Path parameters
idstring (uuid)requiredDestination id.
Responses
- 202
Queued: the result shows on the destination (
failing_since). - 401
The token is missing, has expired or has been revoked.
application/problem+json
Problem - 403
The token lacks the scope the route requires, or the site doesn’t belong to the client.
application/problem+json
Problem - 404
It doesn’t exist.
application/problem+json
Problem - 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-Aftersays when to retry.application/problem+json
Problem
Request · 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();Response 202
HTTP 202What 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
POSTYour destination · alert.*
Your destination receives it; it’s signed and 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 Event
idstring (uuid)requiredFor deduplication: delivery is “at least once”.
Possible values:
alert.createdalert.acknowledgedalert.resolvedalert.reopenedalert.worker_acknowledgedmuster.startedmuster.kind_changedmuster.phase_changedmuster.targets_changedmuster.updatedmuster.worker_missingmuster.endedapi_versionstringrequiredPossible values:
2026-09-24created_atstring (date-time)requiredorganization_idstring (uuid) | nullrequiredThe site’s company.
nullin test events.site_idstring (uuid)requireddata{ alert } | { muster }requiredThe complete object exactly as the
GETwould return it at that moment.One of 2 shapes
- variant 1
Fields of Alert22
idstring (uuid)requiredPossible values:
sosfall_detectedrestricted_zone_entryforbidden_zone_entrypermit_expired_insidepermit_closed_insideheat_stressevacuation_helpworker_call_requestdevice_offlinelow_batteryearthquaketsunami_riskPossible values:
lowmediumhighcriticalPossible values:
unacknowledgedacknowledgedresolvedtitlestringrequiredWith
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 theexternal_id, with no name.date_createdstring (date-time)requireddate_last_modifiedstring (date-time)requiredFor ordering: webhooks don’t guarantee order, and anything older than what you already have is discarded.
Fields of SiteRef2
idstring (uuid)requirednamestringrequired
Fields of ZoneRef2
idstring (uuid)requirednamestringrequired
Fields of WorkerRef4
idstring (uuid)requiredexternal_idstring | nullrequiredThe worker’s code at the company (the one in its personnel system).
nullif not given.namestringFull name. Only if the contract includes it; otherwise the property doesn’t appear.
contractorstring | nullrequired
Fields of DeviceRef4
idstring (uuid)requiredserialstringrequiredbatteryinteger | nullrequiredfrom 0 to 100
date_last_seenstring (date-time) | nullrequired
Fields of Location7
latitudenumberrequiredfrom -90 to 90
longitudenumberrequiredfrom -180 to 180
uncertainty_mnumber | nullrequiredUncertainty radius in metres.
nullif unknown: not knowing it is not the same as it being perfect.minimum 0
sourcestringrequiredzone: the person was in a privacy zone and the point is the zone’s, not theirs.Possible values:
gpsnetworkzonemaskedbooleanrequiredThe point is the representative one for a privacy zone.
position_date_utcstring (date-time)requiredheightobject | nullrequiredHeight above ground by barometer, when it can be stated;
nullif not.
location_withheldbooleanrequiredThe 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.
One of 9 shapes
triggerstringrequiredHow it was requested: on the watch screen, with the physical button, after an unanswered fall, or from the control room.
Possible values:
watch_buttonwatch_hardware_buttonafter_fallcontrol_room
escalated_to_sosbooleanrequiredDid not answer the watch’s “Are you OK?” and it escalated to SOS.
zone_typestringrequiredPossible values:
restrictedforbiddenpermit_referencestring | nullrequiredThe permit to work (job safety analysis, JSA) in force, if there was one.
Nothing derived from heart rate: vital signs don’t leave through the API, not even aggregated.
actionstringrequiredWhat the watch has told them: stop, drink and find shade. There are no levels.
Possible values:
stop_and_restheat_index_cnumber | nullrequiredThe plant’s heat index (°C).
sun_exposure_minutesinteger | nullrequired
reasonstring | nullrequiredWhat they said on the watch: trapped, injured or helping someone else.
Possible values:
trappedinjuredhelpingmuster_idstring (uuid)required
voice_notebooleanrequiredWith no coverage, the watch recorded a voice message (it can be heard in the dashboard).
minutes_silentintegerrequired
battery_pctintegerrequiredfrom 0 to 100
magnitudenumber | nullrequireddistance_kmnumber | nullrequiredofficialbooleanrequiredfalse: a SafeOnuba estimate with no official warning.true: an official bulletin confirmed by a person.felt_at_sitebooleanrequiredetastring (date-time) | nullrequiredEstimated arrival of the wave (tsunami).
Fields of Nearby5
kindstringrequiredPossible values:
personvehicleworkerobjectrequiredvehicle_typestring | nullrequireddistance_mintegerrequiredreported_atstring (date-time)required
Fields of VehicleWarning4
atstring (date-time)requiredvehicle_typestring | nullrequireddistance_mintegerrequiredworker_acknowledged_atstring (date-time) | nullrequired
worker_acknowledged_atstring (date-time) | nullrequiredThe worker pressed “Got it” on their watch (watch time).
Fields of ActionBy2
atstring (date-time)requiredbystringrequiredWho: a person from the dashboard, or “Client · Operator (via integration)”.
resolvedobject | nullrequiredResolved: by whom, when and with what note.
Fields3
atstring (date-time)requiredbystringrequiredWho: a person from the dashboard, or “Client · Operator (via integration)”.
notestring | nullrequired
assigned_tostring | nullrequiredsimulatedbooleanrequiredRaised in a drill or with simulated watches.
panel_urlstring (uri)required
- variant 2
Fields of Muster13
idstring (uuid)requiredFields of SiteRef2
idstring (uuid)requirednamestringrequired
statusstringrequiredPossible values:
activecompletedcancelledphasestringrequiredinspecting: headcount complete and the control room is checking the plant; watches say “do not go back until further notice”.Possible values:
evacuatinginspectingPossible values:
generaltsunamiearthquakereasonstring | nullrequiredstarted_atstring (date-time)requiredended_atstring (date-time) | nullrequireddeadlineobject | nullrequiredFields2
atstring (date-time)requiredsourcestringrequiredTsunami: the time of an official warning, or the ceiling in the zone’s plan. Never a physical calculation.
Possible values:
officialplan_estimate
originobject | nullrequiredWhere the emergency comes from.
nullin an evacuation with no origin (a general drill).Fields5
latitudenumberrequiredlongitudenumberrequiredradius_mintegerrequiredlabelstring | nullrequired
targets_revisionintegerrequiredGoes up every time the targets change (ruling out, restoring, change of type).
totalsobjectrequiredThe 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.Fields7
expectedintegerrequiredsafeintegerrequiredhelpinteger | nullrequiredwrong_pointinteger | nullrequiredpendinginteger | nullrequiredno_signalinteger | nullrequirednot_worninteger | nullrequired
The targets for the type, ruled-out ones included.
Fields of AssemblyPoint5
idstring (uuid)requirednamestringrequiredstatusstringrequiredPossible values:
availableexcludedcountinteger | nullrequiredPeople safe at this point.
nullin an ended evacuation: on ending, only the total is kept.excludedobject | nullrequired
Responses
- 200
Received. Any
2xxwill do.
Body your destination receives
{
"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"
}
}
}How to check it comes from SafeOnuba: verify the signature.
Changes to an evacuation
POSTYour destination · muster.*
Your destination receives it; it’s signed and 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 Event
idstring (uuid)requiredFor deduplication: delivery is “at least once”.
Possible values:
alert.createdalert.acknowledgedalert.resolvedalert.reopenedalert.worker_acknowledgedmuster.startedmuster.kind_changedmuster.phase_changedmuster.targets_changedmuster.updatedmuster.worker_missingmuster.endedapi_versionstringrequiredPossible values:
2026-09-24created_atstring (date-time)requiredorganization_idstring (uuid) | nullrequiredThe site’s company.
nullin test events.site_idstring (uuid)requireddata{ alert } | { muster }requiredThe complete object exactly as the
GETwould return it at that moment.One of 2 shapes
- variant 1
Fields of Alert22
idstring (uuid)requiredPossible values:
sosfall_detectedrestricted_zone_entryforbidden_zone_entrypermit_expired_insidepermit_closed_insideheat_stressevacuation_helpworker_call_requestdevice_offlinelow_batteryearthquaketsunami_riskPossible values:
lowmediumhighcriticalPossible values:
unacknowledgedacknowledgedresolvedtitlestringrequiredWith
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 theexternal_id, with no name.date_createdstring (date-time)requireddate_last_modifiedstring (date-time)requiredFor ordering: webhooks don’t guarantee order, and anything older than what you already have is discarded.
Fields of SiteRef2
idstring (uuid)requirednamestringrequired
Fields of ZoneRef2
idstring (uuid)requirednamestringrequired
Fields of WorkerRef4
idstring (uuid)requiredexternal_idstring | nullrequiredThe worker’s code at the company (the one in its personnel system).
nullif not given.namestringFull name. Only if the contract includes it; otherwise the property doesn’t appear.
contractorstring | nullrequired
Fields of DeviceRef4
idstring (uuid)requiredserialstringrequiredbatteryinteger | nullrequiredfrom 0 to 100
date_last_seenstring (date-time) | nullrequired
Fields of Location7
latitudenumberrequiredfrom -90 to 90
longitudenumberrequiredfrom -180 to 180
uncertainty_mnumber | nullrequiredUncertainty radius in metres.
nullif unknown: not knowing it is not the same as it being perfect.minimum 0
sourcestringrequiredzone: the person was in a privacy zone and the point is the zone’s, not theirs.Possible values:
gpsnetworkzonemaskedbooleanrequiredThe point is the representative one for a privacy zone.
position_date_utcstring (date-time)requiredheightobject | nullrequiredHeight above ground by barometer, when it can be stated;
nullif not.
location_withheldbooleanrequiredThe 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.
One of 9 shapes
triggerstringrequiredHow it was requested: on the watch screen, with the physical button, after an unanswered fall, or from the control room.
Possible values:
watch_buttonwatch_hardware_buttonafter_fallcontrol_room
escalated_to_sosbooleanrequiredDid not answer the watch’s “Are you OK?” and it escalated to SOS.
zone_typestringrequiredPossible values:
restrictedforbiddenpermit_referencestring | nullrequiredThe permit to work (job safety analysis, JSA) in force, if there was one.
Nothing derived from heart rate: vital signs don’t leave through the API, not even aggregated.
actionstringrequiredWhat the watch has told them: stop, drink and find shade. There are no levels.
Possible values:
stop_and_restheat_index_cnumber | nullrequiredThe plant’s heat index (°C).
sun_exposure_minutesinteger | nullrequired
reasonstring | nullrequiredWhat they said on the watch: trapped, injured or helping someone else.
Possible values:
trappedinjuredhelpingmuster_idstring (uuid)required
voice_notebooleanrequiredWith no coverage, the watch recorded a voice message (it can be heard in the dashboard).
minutes_silentintegerrequired
battery_pctintegerrequiredfrom 0 to 100
magnitudenumber | nullrequireddistance_kmnumber | nullrequiredofficialbooleanrequiredfalse: a SafeOnuba estimate with no official warning.true: an official bulletin confirmed by a person.felt_at_sitebooleanrequiredetastring (date-time) | nullrequiredEstimated arrival of the wave (tsunami).
Fields of Nearby5
kindstringrequiredPossible values:
personvehicleworkerobjectrequiredvehicle_typestring | nullrequireddistance_mintegerrequiredreported_atstring (date-time)required
Fields of VehicleWarning4
atstring (date-time)requiredvehicle_typestring | nullrequireddistance_mintegerrequiredworker_acknowledged_atstring (date-time) | nullrequired
worker_acknowledged_atstring (date-time) | nullrequiredThe worker pressed “Got it” on their watch (watch time).
Fields of ActionBy2
atstring (date-time)requiredbystringrequiredWho: a person from the dashboard, or “Client · Operator (via integration)”.
resolvedobject | nullrequiredResolved: by whom, when and with what note.
Fields3
atstring (date-time)requiredbystringrequiredWho: a person from the dashboard, or “Client · Operator (via integration)”.
notestring | nullrequired
assigned_tostring | nullrequiredsimulatedbooleanrequiredRaised in a drill or with simulated watches.
panel_urlstring (uri)required
- variant 2
Fields of Muster13
idstring (uuid)requiredFields of SiteRef2
idstring (uuid)requirednamestringrequired
statusstringrequiredPossible values:
activecompletedcancelledphasestringrequiredinspecting: headcount complete and the control room is checking the plant; watches say “do not go back until further notice”.Possible values:
evacuatinginspectingPossible values:
generaltsunamiearthquakereasonstring | nullrequiredstarted_atstring (date-time)requiredended_atstring (date-time) | nullrequireddeadlineobject | nullrequiredFields2
atstring (date-time)requiredsourcestringrequiredTsunami: the time of an official warning, or the ceiling in the zone’s plan. Never a physical calculation.
Possible values:
officialplan_estimate
originobject | nullrequiredWhere the emergency comes from.
nullin an evacuation with no origin (a general drill).Fields5
latitudenumberrequiredlongitudenumberrequiredradius_mintegerrequiredlabelstring | nullrequired
targets_revisionintegerrequiredGoes up every time the targets change (ruling out, restoring, change of type).
totalsobjectrequiredThe 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.Fields7
expectedintegerrequiredsafeintegerrequiredhelpinteger | nullrequiredwrong_pointinteger | nullrequiredpendinginteger | nullrequiredno_signalinteger | nullrequirednot_worninteger | nullrequired
The targets for the type, ruled-out ones included.
Fields of AssemblyPoint5
idstring (uuid)requirednamestringrequiredstatusstringrequiredPossible values:
availableexcludedcountinteger | nullrequiredPeople safe at this point.
nullin an ended evacuation: on ending, only the total is kept.excludedobject | nullrequired
Responses
- 200
Received. Any
2xxwill do.
Body your destination receives
{
"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"
}
}
}How to check it comes from SafeOnuba: verify the signature.