{
  "openapi": "3.1.0",
  "info": {
    "title": "SafeOnuba API",
    "version": "v1 (2026-09-24)",
    "description": "SafeOnuba alerts and evacuations for the customer’s control room system.\n\nThree rules: the API **sees the same as a dashboard user** with those permissions (the “emergencies only” privacy veil, the privacy zones and the contractor scope all apply in the same way); there are **no vital signs**, not even aggregated; and in v1 it **does not start evacuations**: the system proposes, the person decides.\n\nEvent types: `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`."
  },
  "servers": [
    {
      "url": "https://api.safeonuba.com",
      "description": "Production"
    },
    {
      "url": "https://sandbox.api.safeonuba.com",
      "description": "Testing, with simulated watches"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "OAuth2 `client_credentials`: an opaque token valid for one hour."
    },
    {
      "name": "Sites",
      "description": "The client’s sites and their zones."
    },
    {
      "name": "Alerts",
      "description": "What has happened, and acknowledging or resolving it from the customer’s control room."
    },
    {
      "name": "Evacuations",
      "description": "Status, headcount by status and by muster point, ruled-out points and who is missing."
    },
    {
      "name": "Workers",
      "description": "Workers and watches."
    },
    {
      "name": "Positions",
      "description": "Only those the privacy veil allows."
    },
    {
      "name": "Events",
      "description": "Replay the events of the last 30 days."
    },
    {
      "name": "Webhooks",
      "description": "Destinations and the format of what is sent to them."
    },
    {
      "name": "Sandbox",
      "description": "Test scenarios with simulated watches, so you can integrate without watches. Only on `sandbox.api.safeonuba.com`."
    }
  ],
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "One client per company; the token says which scopes it has. Every request is recorded in the audit log.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/v1/oauth/token",
            "scopes": {
              "alerts:read": "Read alerts and zones.",
              "alerts:write": "Acknowledge and resolve alerts (always with the operator).",
              "positions:read": "Positions the privacy veil allows.",
              "musters:read": "Evacuations and headcount.",
              "workers:read": "Workers and watches.",
              "webhooks:manage": "Webhook destinations."
            }
          }
        }
      }
    },
    "schemas": {
      "Event": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
            "description": "For deduplication: delivery is “at least once”."
          },
          "type": {
            "$ref": "#/components/schemas/EventType"
          },
          "api_version": {
            "type": "string",
            "enum": [
              "2026-09-24"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "organization_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
            "description": "The site’s company. `null` in test events."
          },
          "site_id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "data": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "alert": {
                    "$ref": "#/components/schemas/Alert"
                  }
                },
                "required": [
                  "alert"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "muster": {
                    "$ref": "#/components/schemas/Muster"
                  }
                },
                "required": [
                  "muster"
                ]
              }
            ],
            "description": "The complete object exactly as the `GET` would return it at that moment."
          }
        },
        "required": [
          "id",
          "type",
          "api_version",
          "created_at",
          "organization_id",
          "site_id",
          "data"
        ]
      },
      "EventType": {
        "type": "string",
        "enum": [
          "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"
        ],
        "description": "`muster.updated`: the headcount has changed (checked every 5 s, only emitted if it changes). `muster.targets_changed`: the control room has ruled out or restored a muster point."
      },
      "Alert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "type": {
            "$ref": "#/components/schemas/AlertType"
          },
          "severity": {
            "$ref": "#/components/schemas/Severity"
          },
          "status": {
            "$ref": "#/components/schemas/AlertStatus"
          },
          "title": {
            "type": "string",
            "description": "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.",
            "example": "SOS activado — Ana Ejemplo"
          },
          "date_created": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "date_last_modified": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z",
            "description": "For ordering: webhooks don’t guarantee order, and anything older than what you already have is discarded."
          },
          "site": {
            "$ref": "#/components/schemas/SiteRef"
          },
          "zone": {
            "$ref": "#/components/schemas/ZoneRef"
          },
          "worker": {
            "$ref": "#/components/schemas/WorkerRef"
          },
          "device": {
            "$ref": "#/components/schemas/DeviceRef"
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "location_withheld": {
            "type": "boolean",
            "description": "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.",
            "example": false
          },
          "details": {
            "$ref": "#/components/schemas/AlertDetails"
          },
          "nearby": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Nearby"
            }
          },
          "vehicle_warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VehicleWarning"
            }
          },
          "worker_acknowledged_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z",
            "description": "The worker pressed “Got it” on their watch (watch time)."
          },
          "acknowledged": {
            "$ref": "#/components/schemas/ActionBy"
          },
          "resolved": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ActionBy"
              },
              {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "note": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "example": "Falsa alarma: pulsado sin querer."
                  }
                },
                "required": [
                  "note"
                ]
              }
            ],
            "description": "Resolved: by whom, when and with what note."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "example": "Berta Ejemplo"
          },
          "simulated": {
            "type": "boolean",
            "description": "Raised in a drill or with simulated watches.",
            "example": false
          },
          "panel_url": {
            "type": "string",
            "format": "uri",
            "example": "https://app.safeonuba.com/alertas/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          }
        },
        "required": [
          "id",
          "type",
          "severity",
          "status",
          "title",
          "date_created",
          "date_last_modified",
          "site",
          "zone",
          "worker",
          "device",
          "location",
          "location_withheld",
          "details",
          "nearby",
          "vehicle_warnings",
          "worker_acknowledged_at",
          "acknowledged",
          "resolved",
          "assigned_to",
          "simulated",
          "panel_url"
        ],
        "example": {
          "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"
        }
      },
      "AlertType": {
        "type": "string",
        "enum": [
          "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"
        ],
        "description": "What has happened. `earthquake` and `tsunami_risk` concern the whole plant: they carry no `worker` or `device`. A nearby vehicle is **not** an alert: it appears inside an incident’s alert (`nearby`, `vehicle_warnings`)."
      },
      "Severity": {
        "type": "string",
        "enum": [
          "low",
          "medium",
          "high",
          "critical"
        ],
        "description": "Severity: `low` (information), `medium` (warning), `high` (serious), `critical` (critical: SOS, fall, “I can’t evacuate”)."
      },
      "AlertStatus": {
        "type": "string",
        "enum": [
          "unacknowledged",
          "acknowledged",
          "resolved"
        ],
        "description": "`acknowledged`: an operator has taken charge. It is not the same as `worker_acknowledged_at`, which means the worker has seen the alert on their watch."
      },
      "SiteRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "name": {
            "type": "string",
            "example": "Planta Ejemplo — simulación"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ZoneRef": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "name": {
            "type": "string",
            "example": "Almacenamiento H₂ (ATEX)"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "WorkerRef": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The worker’s code at the company (the one in its personnel system). `null` if not given.",
            "example": "EMP-004512"
          },
          "name": {
            "type": "string",
            "description": "Full name. **Only** if the contract includes it; otherwise the property doesn’t appear.",
            "example": "Ana Ejemplo"
          },
          "contractor": {
            "type": [
              "string",
              "null"
            ],
            "example": "Contrata Ejemplo A"
          }
        },
        "required": [
          "id",
          "external_id",
          "contractor"
        ]
      },
      "DeviceRef": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "serial": {
            "type": "string",
            "example": "OS-W-0005"
          },
          "battery": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "example": 64
          },
          "date_last_seen": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          }
        },
        "required": [
          "id",
          "serial",
          "battery",
          "date_last_seen"
        ]
      },
      "Location": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "example": 40.00121
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "example": -3.00214
          },
          "uncertainty_m": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Uncertainty radius in metres. `null` if unknown: not knowing it is not the same as it being perfect.",
            "example": 8
          },
          "source": {
            "type": "string",
            "enum": [
              "gps",
              "network",
              "zone"
            ],
            "description": "`zone`: the person was in a privacy zone and the point is the zone’s, not theirs.",
            "example": "gps"
          },
          "masked": {
            "type": "boolean",
            "description": "The point is the representative one for a privacy zone.",
            "example": false
          },
          "position_date_utc": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "height": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "height_m": {
                "type": "number",
                "example": 7.6
              },
              "uncertainty_m": {
                "type": "number",
                "example": 1
              },
              "building": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Nave de proceso"
              },
              "floor": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Planta 1"
              }
            },
            "required": [
              "height_m",
              "uncertainty_m",
              "building",
              "floor"
            ],
            "description": "Height above ground by barometer, when it can be stated; `null` if not."
          }
        },
        "required": [
          "latitude",
          "longitude",
          "uncertainty_m",
          "source",
          "masked",
          "position_date_utc",
          "height"
        ]
      },
      "AlertDetails": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/SosDetails"
          },
          {
            "$ref": "#/components/schemas/FallDetails"
          },
          {
            "$ref": "#/components/schemas/ZoneDetails"
          },
          {
            "$ref": "#/components/schemas/HeatStressDetails"
          },
          {
            "$ref": "#/components/schemas/EvacuationHelpDetails"
          },
          {
            "$ref": "#/components/schemas/WorkerCallDetails"
          },
          {
            "$ref": "#/components/schemas/DeviceOfflineDetails"
          },
          {
            "$ref": "#/components/schemas/LowBatteryDetails"
          },
          {
            "$ref": "#/components/schemas/HazardDetails"
          }
        ],
        "description": "Depends on `type`."
      },
      "SosDetails": {
        "type": "object",
        "properties": {
          "trigger": {
            "type": "string",
            "enum": [
              "watch_button",
              "watch_hardware_button",
              "after_fall",
              "control_room"
            ],
            "description": "How it was requested: on the watch screen, with the physical button, after an unanswered fall, or from the control room.",
            "example": "watch_button"
          }
        },
        "required": [
          "trigger"
        ]
      },
      "FallDetails": {
        "type": "object",
        "properties": {
          "escalated_to_sos": {
            "type": "boolean",
            "description": "Did not answer the watch’s “Are you OK?” and it escalated to SOS.",
            "example": true
          }
        },
        "required": [
          "escalated_to_sos"
        ]
      },
      "ZoneDetails": {
        "type": "object",
        "properties": {
          "zone_type": {
            "type": "string",
            "enum": [
              "restricted",
              "forbidden"
            ],
            "example": "forbidden"
          },
          "permit_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "The permit to work (job safety analysis, JSA) in force, if there was one.",
            "example": "PT-0453"
          }
        },
        "required": [
          "zone_type",
          "permit_reference"
        ]
      },
      "HeatStressDetails": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "stop_and_rest"
            ],
            "description": "What the watch has told them: stop, drink and find shade. There are no levels."
          },
          "heat_index_c": {
            "type": [
              "number",
              "null"
            ],
            "description": "The plant’s heat index (°C).",
            "example": 33
          },
          "sun_exposure_minutes": {
            "type": [
              "integer",
              "null"
            ],
            "example": 91
          }
        },
        "required": [
          "action",
          "heat_index_c",
          "sun_exposure_minutes"
        ],
        "description": "Nothing derived from heart rate: vital signs don’t leave through the API, not even aggregated."
      },
      "EvacuationHelpDetails": {
        "type": "object",
        "properties": {
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "trapped",
              "injured",
              "helping",
              null
            ],
            "description": "What they said on the watch: trapped, injured or helping someone else.",
            "example": "trapped"
          },
          "muster_id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          }
        },
        "required": [
          "reason",
          "muster_id"
        ]
      },
      "WorkerCallDetails": {
        "type": "object",
        "properties": {
          "voice_note": {
            "type": "boolean",
            "description": "With no coverage, the watch recorded a voice message (it can be heard in the dashboard).",
            "example": false
          }
        },
        "required": [
          "voice_note"
        ]
      },
      "DeviceOfflineDetails": {
        "type": "object",
        "properties": {
          "minutes_silent": {
            "type": "integer",
            "example": 17
          }
        },
        "required": [
          "minutes_silent"
        ]
      },
      "LowBatteryDetails": {
        "type": "object",
        "properties": {
          "battery_pct": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "example": 12
          }
        },
        "required": [
          "battery_pct"
        ]
      },
      "HazardDetails": {
        "type": "object",
        "properties": {
          "magnitude": {
            "type": [
              "number",
              "null"
            ],
            "example": 5.2
          },
          "distance_km": {
            "type": [
              "number",
              "null"
            ],
            "example": 180
          },
          "official": {
            "type": "boolean",
            "description": "`false`: a SafeOnuba estimate with no official warning. `true`: an official bulletin confirmed by a person.",
            "example": false
          },
          "felt_at_site": {
            "type": "boolean",
            "example": false
          },
          "eta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z",
            "description": "Estimated arrival of the wave (tsunami)."
          }
        },
        "required": [
          "magnitude",
          "distance_km",
          "official",
          "felt_at_site",
          "eta"
        ]
      },
      "Nearby": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "person",
              "vehicle"
            ],
            "example": "vehicle"
          },
          "worker": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WorkerRef"
              },
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
                    "description": "`null` in alerts from before 24 September 2026, when it was not yet being copied."
                  }
                }
              }
            ]
          },
          "vehicle_type": {
            "type": [
              "string",
              "null"
            ],
            "example": "Grúa móvil"
          },
          "distance_m": {
            "type": "integer",
            "example": 18
          },
          "reported_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          }
        },
        "required": [
          "kind",
          "worker",
          "vehicle_type",
          "distance_m",
          "reported_at"
        ],
        "description": "Who was within 100 m when a critical alert was raised (SOS, fall). Distance, never position; anyone in a privacy zone doesn’t count."
      },
      "VehicleWarning": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "vehicle_type": {
            "type": [
              "string",
              "null"
            ],
            "example": "Carretilla"
          },
          "distance_m": {
            "type": "integer",
            "example": 8
          },
          "worker_acknowledged_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          }
        },
        "required": [
          "at",
          "vehicle_type",
          "distance_m",
          "worker_acknowledged_at"
        ],
        "description": "Nearby-vehicle warnings that person received during the incident window. They are warnings, not alerts."
      },
      "ActionBy": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "by": {
            "type": "string",
            "description": "Who: a person from the dashboard, or “Client · Operator (via integration)”.",
            "example": "Cliente Ejemplo · E. Ejemplo (vía integración)"
          }
        },
        "required": [
          "at",
          "by"
        ]
      },
      "Muster": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "site": {
            "$ref": "#/components/schemas/SiteRef"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "cancelled"
            ],
            "example": "active"
          },
          "phase": {
            "type": "string",
            "enum": [
              "evacuating",
              "inspecting"
            ],
            "description": "`inspecting`: headcount complete and the control room is checking the plant; watches say “do not go back until further notice”.",
            "example": "evacuating"
          },
          "kind": {
            "$ref": "#/components/schemas/MusterKind"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "example": "Fuga en Mantenimiento"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "deadline": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "at": {
                "type": "string",
                "format": "date-time",
                "example": "2026-09-24T11:04:12Z"
              },
              "source": {
                "type": "string",
                "enum": [
                  "official",
                  "plan_estimate"
                ],
                "description": "Tsunami: the time of an official warning, or the ceiling in the zone’s plan. Never a physical calculation."
              }
            },
            "required": [
              "at",
              "source"
            ]
          },
          "origin": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "latitude": {
                "type": "number",
                "example": 39.97894
              },
              "longitude": {
                "type": "number",
                "example": -2.96877
              },
              "radius_m": {
                "type": "integer",
                "example": 400
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Mantenimiento — permiso PT-0453"
              },
              "zone": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ZoneRef"
                  },
                  {
                    "description": "If the origin is a whole zone: the radius is measured from its edge."
                  }
                ]
              }
            },
            "required": [
              "latitude",
              "longitude",
              "radius_m",
              "label",
              "zone"
            ],
            "description": "Where the emergency comes from. `null` in an evacuation with no origin (a general drill)."
          },
          "targets_revision": {
            "type": "integer",
            "description": "Goes up every time the targets change (ruling out, restoring, change of type).",
            "example": 3
          },
          "totals": {
            "type": "object",
            "properties": {
              "expected": {
                "type": "integer"
              },
              "safe": {
                "type": "integer"
              },
              "help": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "wrong_point": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "pending": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "no_signal": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "not_worn": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "expected",
              "safe",
              "help",
              "wrong_point",
              "pending",
              "no_signal",
              "not_worn"
            ],
            "description": "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`."
          },
          "assembly_points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssemblyPoint"
            },
            "description": "The targets for the type, ruled-out ones included."
          }
        },
        "required": [
          "id",
          "site",
          "status",
          "phase",
          "kind",
          "reason",
          "started_at",
          "ended_at",
          "deadline",
          "origin",
          "targets_revision",
          "totals",
          "assembly_points"
        ],
        "example": {
          "id": "4fb94227-7afb-442d-a672-6fd82069518f",
          "site": {
            "id": "11111111-1111-1111-1111-111111111111",
            "name": "Planta Ejemplo — simulación"
          },
          "status": "active",
          "phase": "evacuating",
          "kind": "general",
          "reason": "Fuga en Mantenimiento",
          "started_at": "2026-09-24T18:31:06Z",
          "ended_at": null,
          "deadline": null,
          "origin": {
            "latitude": 39.97894,
            "longitude": -2.96877,
            "radius_m": 400,
            "label": "Mantenimiento — permiso PT-0453",
            "zone": {
              "id": "22222222-2222-2222-2222-222222222203",
              "name": "Mantenimiento — permiso PT-0453"
            }
          },
          "targets_revision": 3,
          "totals": {
            "expected": 9,
            "safe": 6,
            "help": 0,
            "wrong_point": 0,
            "pending": 2,
            "no_signal": 0,
            "not_worn": 1
          },
          "assembly_points": [
            {
              "id": "22222222-2222-2222-2222-222222222204",
              "name": "Punto de reunión PR-1",
              "status": "excluded",
              "count": 0,
              "excluded": {
                "reason": "A 371 m del origen (radio 400 m)",
                "recommended": true,
                "at": "2026-09-24T18:31:06Z",
                "by": "Elena Ejemplo"
              }
            },
            {
              "id": "22222222-2222-2222-2222-222222222205",
              "name": "Punto de reunión PR-2",
              "status": "excluded",
              "count": 0,
              "excluded": {
                "reason": "A sotavento: viento del SO a 9 km/h",
                "recommended": true,
                "at": "2026-09-24T18:31:42Z",
                "by": "Elena Ejemplo"
              }
            },
            {
              "id": "f6040b41-be81-4bf3-af68-bfaf3e284c54",
              "name": "Punto de reunión PR-3 · Zona alta",
              "status": "available",
              "count": 6,
              "excluded": null
            }
          ]
        }
      },
      "MusterKind": {
        "type": "string",
        "enum": [
          "general",
          "tsunami",
          "earthquake"
        ]
      },
      "AssemblyPoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "name": {
            "type": "string",
            "example": "Punto de reunión PR-2"
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "excluded"
            ],
            "example": "available"
          },
          "count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "People safe at this point. `null` in an ended evacuation: on ending, only the total is kept.",
            "example": 115
          },
          "excluded": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "reason": {
                "type": "string",
                "example": "A sotavento: viento del SO a 9 km/h"
              },
              "recommended": {
                "type": "boolean",
                "description": "The system had recommended it (radius or wind). Whoever rules it out is always a person.",
                "example": true
              },
              "at": {
                "type": "string",
                "format": "date-time",
                "example": "2026-09-24T11:04:12Z"
              },
              "by": {
                "type": "string",
                "example": "Elena Ejemplo"
              }
            },
            "required": [
              "reason",
              "recommended",
              "at",
              "by"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "count",
          "excluded"
        ]
      },
      "TokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "example": "at_9c1e…"
          },
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expires_in": {
            "type": "number",
            "enum": [
              3600
            ]
          },
          "scope": {
            "type": "string",
            "example": "alerts:read alerts:write musters:read"
          }
        },
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ]
      },
      "OAuthError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "unauthorized_client",
              "unsupported_grant_type",
              "invalid_scope"
            ]
          },
          "error_description": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "description": "Token error, in the RFC 6749 §5.2 format."
      },
      "TokenRequest": {
        "type": "object",
        "properties": {
          "grant_type": {
            "type": "string",
            "enum": [
              "client_credentials"
            ]
          },
          "client_id": {
            "type": "string",
            "example": "cliente-ejemplo"
          },
          "client_secret": {
            "type": "string",
            "example": "cs_2b7a…"
          },
          "scope": {
            "type": "string",
            "description": "Space-separated; a subset of the client’s. Without it, all of the client’s.",
            "example": "alerts:read alerts:write musters:read"
          }
        },
        "required": [
          "grant_type",
          "client_id",
          "client_secret"
        ]
      },
      "SiteList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Site"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "name": {
            "type": "string",
            "example": "Planta Ejemplo — simulación"
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "example": "Polígono industrial de ejemplo"
          },
          "privacy_mode": {
            "type": "string",
            "enum": [
              "continuous",
              "on_demand"
            ],
            "description": "`on_demand` (“emergencies only”): a person’s position is only visible with an open alert from `unveil_min_severity`, during an evacuation, or with an approved request. The API obeys the same rule as the dashboard.",
            "example": "on_demand"
          },
          "unveil_min_severity": {
            "$ref": "#/components/schemas/Severity"
          }
        },
        "required": [
          "id",
          "name",
          "location",
          "privacy_mode",
          "unveil_min_severity"
        ]
      },
      "Problem": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "URI that identifies the problem type.",
            "example": "https://api.safeonuba.com/problems/insufficient-scope"
          },
          "title": {
            "type": "string",
            "example": "Falta un permiso del token"
          },
          "status": {
            "type": "integer",
            "example": 403
          },
          "detail": {
            "type": "string",
            "example": "Esta petición necesita el scope alerts:write."
          },
          "instance": {
            "type": "string",
            "example": "/v1/alerts/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "code": {
            "type": "string",
            "description": "Stable error code, to program against. The text may change; this won’t.",
            "example": "insufficient_scope"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "description": "Error in RFC 9457 format (`application/problem+json`), with the real HTTP status code."
      },
      "ZoneList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Zone"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Zone": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "name": {
            "type": "string",
            "example": "Punto de reunión PR-3 · Zona alta"
          },
          "type": {
            "type": "string",
            "enum": [
              "work_area",
              "restricted",
              "forbidden",
              "assembly_point",
              "privacy",
              "building"
            ],
            "example": "assembly_point"
          },
          "geometry": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "Polygon"
                ]
              },
              "coordinates": {
                "type": "array",
                "items": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "prefixItems": [
                      {
                        "type": "number"
                      },
                      {
                        "type": "number"
                      }
                    ]
                  }
                }
              }
            },
            "required": [
              "type",
              "coordinates"
            ],
            "description": "GeoJSON polygon (WGS84). `null` for privacy zones: the name and type are given, not where they are (pending validation with the committee)."
          },
          "assembly_point": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "serves": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "general",
                    "tsunami",
                    "earthquake"
                  ]
                }
              },
              "elevation_m": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "elevation_source": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "dem",
                  "verified",
                  null
                ]
              },
              "vertical": {
                "type": "boolean",
                "description": "Vertical refuge (a building)."
              },
              "capacity": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "serves",
              "elevation_m",
              "elevation_source",
              "vertical",
              "capacity"
            ],
            "description": "Only on muster points."
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "geometry",
          "assembly_point"
        ],
        "example": {
          "id": "f6040b41-be81-4bf3-af68-bfaf3e284c54",
          "name": "Punto de reunión PR-3 · Zona alta",
          "type": "assembly_point",
          "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [
                  -2.9825,
                  40.0081
                ],
                [
                  -2.9818,
                  40.0081
                ],
                [
                  -2.9818,
                  40.0076
                ],
                [
                  -2.9825,
                  40.0076
                ],
                [
                  -2.9825,
                  40.0081
                ]
              ]
            ]
          },
          "assembly_point": {
            "serves": [
              "general",
              "tsunami"
            ],
            "elevation_m": 50,
            "elevation_source": "verified",
            "vertical": false,
            "capacity": 200
          }
        }
      },
      "AlertPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Alert"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page; `null` if there are no more.",
            "example": "eyJ0IjoiMjAyNi0wOS0yNFQxMTowNDoxMloifQ"
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "AlertUpdate": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "acknowledged",
              "resolved"
            ],
            "example": "acknowledged"
          },
          "resolution_reason": {
            "type": "string",
            "maxLength": 200,
            "description": "On resolving: why. Free text; it goes in the alert’s timeline next to the note.",
            "example": "Falsa alarma"
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "example": "Confirmado por radio con el encargado."
          },
          "actor": {
            "$ref": "#/components/schemas/Actor"
          }
        },
        "required": [
          "status",
          "actor"
        ],
        "description": "Two states, `acknowledged` and `resolved`, plus the operator. Acknowledging self-assigns the alert if nobody was handling it, as in the dashboard."
      },
      "Actor": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "example": "E. Ejemplo"
          },
          "external_id": {
            "type": "string",
            "maxLength": 64,
            "example": "OP-117"
          }
        },
        "required": [
          "name"
        ],
        "description": "The operator in the customer’s control room who takes the action. Required: “acknowledged” measures a person’s response, and without a name the timeline would only show the client’s."
      },
      "MusterPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Muster"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page; `null` if there are no more.",
            "example": "eyJ0IjoiMjAyNi0wOS0yNFQxMTowNDoxMloifQ"
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "MissingWorkerList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MissingWorker"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "MissingWorker": {
        "type": "object",
        "properties": {
          "worker": {
            "$ref": "#/components/schemas/WorkerRef"
          },
          "device": {
            "$ref": "#/components/schemas/DeviceRef"
          },
          "status": {
            "type": "string",
            "enum": [
              "help",
              "wrong_point",
              "pending",
              "no_signal",
              "not_worn"
            ]
          },
          "needs_rescue": {
            "type": "boolean",
            "description": "Has an open SOS or fall and has not arrived: cannot leave on their own.",
            "example": false
          },
          "help_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "trapped",
              "injured",
              "helping",
              null
            ]
          },
          "assembly_point": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
              },
              "name": {
                "type": "string"
              },
              "valid": {
                "type": "boolean"
              }
            },
            "required": [
              "id",
              "name",
              "valid"
            ],
            "description": "The point where they are or said they were (with `wrong_point`, one that is unusable)."
          },
          "declared_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          },
          "location": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Location"
              },
              {
                "description": "With the `positions:read` scope."
              }
            ]
          },
          "location_withheld": {
            "type": "boolean"
          }
        },
        "required": [
          "worker",
          "device",
          "status",
          "needs_rescue",
          "help_reason",
          "assembly_point",
          "declared_at",
          "last_seen_at",
          "location",
          "location_withheld"
        ],
        "example": {
          "worker": {
            "id": "33333333-3333-3333-3333-333333333305",
            "external_id": "EMP-004512",
            "contractor": "Contrata Ejemplo A"
          },
          "device": {
            "id": "44444444-4444-4444-4444-444444444405",
            "serial": "OS-W-0005",
            "battery": 61,
            "date_last_seen": "2026-09-24T18:33:40Z"
          },
          "status": "wrong_point",
          "needs_rescue": false,
          "help_reason": null,
          "assembly_point": {
            "id": "22222222-2222-2222-2222-222222222205",
            "name": "Punto de reunión PR-2",
            "valid": false
          },
          "declared_at": "2026-09-24T18:32:10Z",
          "last_seen_at": "2026-09-24T18:33:40Z",
          "location": null,
          "location_withheld": false
        }
      },
      "WorkerPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Worker"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page; `null` if there are no more.",
            "example": "eyJ0IjoiMjAyNi0wOS0yNFQxMTowNDoxMloifQ"
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Worker": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WorkerRef"
          },
          {
            "type": "object",
            "properties": {
              "role": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Andamiero"
              },
              "is_active": {
                "type": "boolean"
              },
              "device": {
                "$ref": "#/components/schemas/DeviceRef"
              },
              "date_last_seen": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "example": "2026-09-24T11:04:12Z",
                "description": "The most recent of the last position and the watch’s last contact."
              }
            },
            "required": [
              "role",
              "is_active",
              "device",
              "date_last_seen"
            ]
          }
        ]
      },
      "DevicePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Device"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page; `null` if there are no more.",
            "example": "eyJ0IjoiMjAyNi0wOS0yNFQxMTowNDoxMloifQ"
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Device": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DeviceRef"
          },
          {
            "type": "object",
            "properties": {
              "model": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Galaxy Watch6"
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive",
                  "maintenance",
                  "lost"
                ],
                "example": "active"
              },
              "worn": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "`null`: not known (no wrist sensor).",
                "example": true
              },
              "worker": {
                "$ref": "#/components/schemas/WorkerRef"
              }
            },
            "required": [
              "model",
              "status",
              "worn",
              "worker"
            ]
          }
        ]
      },
      "PositionList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Position"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Position": {
        "type": "object",
        "properties": {
          "device_id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "worker": {
            "$ref": "#/components/schemas/WorkerRef"
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "in_vehicle": {
            "type": "boolean"
          }
        },
        "required": [
          "device_id",
          "worker",
          "location",
          "in_vehicle"
        ],
        "description": "Only the positions the privacy veil allows at that moment: in “emergencies only”, anyone with nothing open doesn’t appear."
      },
      "EventPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page; `null` if there are no more.",
            "example": "eyJ0IjoiMjAyNi0wOS0yNFQxMTowNDoxMloifQ"
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "WebhookEndpointList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "event_types": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/EventType"
                },
                {
                  "type": "string",
                  "enum": [
                    "alert.*",
                    "muster.*"
                  ]
                }
              ]
            }
          },
          "filters": {
            "type": "object",
            "properties": {
              "site_ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid",
                  "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
                }
              },
              "min_severity": {
                "$ref": "#/components/schemas/Severity"
              },
              "contractors": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "is_active": {
            "type": "boolean"
          },
          "failing_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z",
            "description": "Has been failing since then. The destination is **not** disabled automatically: the next SOS would stop arriving without anyone deciding it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          }
        },
        "required": [
          "id",
          "url",
          "event_types",
          "filters",
          "is_active",
          "failing_since",
          "created_at"
        ]
      },
      "WebhookEndpointCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEndpoint"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "The signing secret. **Shown only this once.**",
                "example": "whsec_6f1c0d9e2b7a4c3f8e5d1a0b9c8e7f6a"
              }
            },
            "required": [
              "secret"
            ]
          }
        ]
      },
      "WebhookEndpointCreate": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "example": "https://sala.cliente.example/safeonuba"
          },
          "event_types": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/EventType"
                },
                {
                  "type": "string",
                  "enum": [
                    "alert.*",
                    "muster.*"
                  ]
                }
              ]
            },
            "minItems": 1,
            "example": [
              "alert.*",
              "muster.*"
            ]
          },
          "filters": {
            "type": "object",
            "properties": {
              "site_ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid",
                  "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
                }
              },
              "min_severity": {
                "$ref": "#/components/schemas/Severity"
              },
              "contractors": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        },
        "required": [
          "url",
          "event_types"
        ]
      },
      "WebhookSecretRotated": {
        "type": "object",
        "properties": {
          "secret": {
            "type": "string",
            "example": "whsec_6f1c0d9e2b7a4c3f8e5d1a0b9c8e7f6a"
          },
          "previous_valid_until": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z"
          }
        },
        "required": [
          "secret",
          "previous_valid_until"
        ]
      },
      "SandboxScenarioRun": {
        "type": "object",
        "properties": {
          "run_id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "scenario": {
            "$ref": "#/components/schemas/SandboxScenario"
          },
          "site_id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
          },
          "resource": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "alert",
                  "muster"
                ]
              },
              "id": {
                "type": "string",
                "format": "uuid",
                "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
              }
            },
            "required": [
              "type",
              "id"
            ],
            "description": "What it has opened: check it with its `GET`, or wait for the webhook."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-24T11:04:12Z",
            "description": "When it closes by itself."
          }
        },
        "required": [
          "run_id",
          "scenario",
          "site_id",
          "resource",
          "ends_at"
        ]
      },
      "SandboxScenario": {
        "type": "string",
        "enum": [
          "sos",
          "fall",
          "heat_stress",
          "evacuation"
        ],
        "description": "- `sos`, `fall`, `heat_stress`: a test person presses SOS, falls or suffers heat stress. The alert goes out (`alert.created`) and, if nobody resolves it first with `PUT /v1/alerts/{id}`, it resolves itself after 10 min (`alert.resolved`).\n- `evacuation`: a general evacuation of 3 min. At 15 s four people arrive at point A; at 35 s someone arrives at B and the control room rules it out (`muster.targets_changed`); at 55 s a person asks for help (`alert.created`, `evacuation_help`); at 95 s whoever was at B moves on to C; at 3 min, all clear (`muster.ended`). In between, `muster.updated` every time the headcount changes."
      },
      "SandboxScenarioRequest": {
        "type": "object",
        "properties": {
          "scenario": {
            "$ref": "#/components/schemas/SandboxScenario"
          },
          "site_id": {
            "type": "string",
            "format": "uuid",
            "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
            "description": "The sandbox site. Without it, the first one of yours that is a sandbox site."
          }
        },
        "required": [
          "scenario"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/v1/oauth/token": {
      "post": {
        "operationId": "createToken",
        "security": [],
        "tags": [
          "Authentication"
        ],
        "summary": "Request a token (client_credentials)",
        "description": "OAuth2 `client_credentials`. The token is opaque, lasts one hour and is revoked immediately when the client is deactivated.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request or scope not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "Incorrect client or secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sites": {
      "get": {
        "operationId": "listSites",
        "tags": [
          "Sites"
        ],
        "summary": "The client’s sites",
        "security": [
          {
            "oauth2": []
          }
        ],
        "description": "**Scope:** any.",
        "responses": {
          "200": {
            "description": "Sites the client can access.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteList"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sites/{id}/zones": {
      "get": {
        "operationId": "listSiteZones",
        "tags": [
          "Sites"
        ],
        "summary": "A site’s zones and muster points",
        "description": "GeoJSON in WGS84. Privacy zones come with name and type but no geometry.\n\n**Scope:** `alerts:read`.",
        "security": [
          {
            "oauth2": [
              "alerts:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Site id."
            },
            "required": true,
            "description": "Site id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Zones in force.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ZoneList"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "The site doesn’t exist or doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/alerts": {
      "get": {
        "operationId": "listAlerts",
        "tags": [
          "Alerts"
        ],
        "summary": "List alerts",
        "security": [
          {
            "oauth2": [
              "alerts:read"
            ]
          }
        ],
        "description": "**Scope:** `alerts:read`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
            },
            "required": false,
            "name": "site_id",
            "in": "query"
          },
          {
            "schema": {
              "$ref": "#/components/schemas/AlertType"
            },
            "required": false,
            "description": "What has happened. `earthquake` and `tsunami_risk` concern the whole plant: they carry no `worker` or `device`. A nearby vehicle is **not** an alert: it appears inside an incident’s alert (`nearby`, `vehicle_warnings`).",
            "name": "type",
            "in": "query"
          },
          {
            "schema": {
              "$ref": "#/components/schemas/AlertStatus"
            },
            "required": false,
            "description": "`acknowledged`: an operator has taken charge. It is not the same as `worker_acknowledged_at`, which means the worker has seen the alert on their watch.",
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Severity"
                },
                {
                  "description": "Minimum severity."
                }
              ]
            },
            "required": false,
            "description": "Minimum severity.",
            "name": "severity",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-09-24T11:04:12Z"
            },
            "required": false,
            "name": "since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-09-24T11:04:12Z"
            },
            "required": false,
            "name": "until",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100,
              "description": "Items per page (maximum 500).",
              "example": 100
            },
            "required": false,
            "description": "Items per page (maximum 500).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` from the previous page."
            },
            "required": false,
            "description": "The `next_cursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts, from the most recent to the oldest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertPage"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/alerts/{id}": {
      "get": {
        "operationId": "getAlert",
        "tags": [
          "Alerts"
        ],
        "summary": "Alert detail",
        "security": [
          {
            "oauth2": [
              "alerts:read"
            ]
          }
        ],
        "description": "**Scope:** `alerts:read`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Alert id."
            },
            "required": true,
            "description": "Alert id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The alert.",
            "content": {
              "application/json": {
                "examples": {
                  "forbidden_zone_entry": {
                    "value": {
                      "id": "7a1e9c40-52d8-4b0f-a3c6-2e9d81f0b7a4",
                      "type": "forbidden_zone_entry",
                      "severity": "high",
                      "status": "unacknowledged",
                      "title": "Entrada en zona prohibida — Almacenamiento H₂ (ATEX) — EMP-003318",
                      "date_created": "2026-09-24T10:12:40Z",
                      "date_last_modified": "2026-09-24T10:12:40Z",
                      "site": {
                        "id": "11111111-1111-1111-1111-111111111111",
                        "name": "Planta Ejemplo — simulación"
                      },
                      "zone": {
                        "id": "22222222-2222-2222-2222-222222222202",
                        "name": "Almacenamiento H₂ (ATEX)"
                      },
                      "worker": {
                        "id": "33333333-3333-3333-3333-333333333303",
                        "external_id": "EMP-003318",
                        "contractor": "Contrata Ejemplo C"
                      },
                      "device": {
                        "id": "44444444-4444-4444-4444-444444444403",
                        "serial": "OS-W-0003",
                        "battery": 81,
                        "date_last_seen": "2026-09-24T10:12:38Z"
                      },
                      "location": {
                        "latitude": 40.00344,
                        "longitude": -3.00561,
                        "uncertainty_m": 5,
                        "source": "gps",
                        "masked": false,
                        "position_date_utc": "2026-09-24T10:12:36Z",
                        "height": {
                          "height_m": 7.6,
                          "uncertainty_m": 1,
                          "building": "Nave de proceso",
                          "floor": "Planta 1"
                        }
                      },
                      "location_withheld": false,
                      "details": {
                        "zone_type": "forbidden",
                        "permit_reference": null
                      },
                      "nearby": [],
                      "vehicle_warnings": [],
                      "worker_acknowledged_at": "2026-09-24T10:12:51Z",
                      "acknowledged": null,
                      "resolved": null,
                      "assigned_to": null,
                      "simulated": true,
                      "panel_url": "https://app.safeonuba.com/alertas/7a1e9c40-52d8-4b0f-a3c6-2e9d81f0b7a4"
                    }
                  },
                  "heat_stress": {
                    "value": {
                      "id": "c3b58f21-0e7a-4d19-8f6b-5a2c9e417d03",
                      "type": "heat_stress",
                      "severity": "high",
                      "status": "acknowledged",
                      "title": "Estrés térmico — parar y descansar",
                      "date_created": "2026-09-24T13:41:05Z",
                      "date_last_modified": "2026-09-24T13:43:30Z",
                      "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",
                        "contractor": "Contrata Ejemplo A"
                      },
                      "device": {
                        "id": "44444444-4444-4444-4444-444444444405",
                        "serial": "OS-W-0005",
                        "battery": 58,
                        "date_last_seen": "2026-09-24T13:41:02Z"
                      },
                      "location": {
                        "latitude": 39.99954,
                        "longitude": -3.00377,
                        "uncertainty_m": 6,
                        "source": "gps",
                        "masked": false,
                        "position_date_utc": "2026-09-24T13:41:00Z",
                        "height": null
                      },
                      "location_withheld": false,
                      "details": {
                        "action": "stop_and_rest",
                        "heat_index_c": 33,
                        "sun_exposure_minutes": 91
                      },
                      "nearby": [],
                      "vehicle_warnings": [],
                      "worker_acknowledged_at": "2026-09-24T13:41:20Z",
                      "acknowledged": {
                        "at": "2026-09-24T13:43:30Z",
                        "by": "Cliente Ejemplo · E. Ejemplo (vía integración)"
                      },
                      "resolved": null,
                      "assigned_to": "Berta Ejemplo",
                      "simulated": true,
                      "panel_url": "https://app.safeonuba.com/alertas/c3b58f21-0e7a-4d19-8f6b-5a2c9e417d03"
                    }
                  },
                  "evacuation_help": {
                    "value": {
                      "id": "5d02c7a9-1b3e-4f80-9c24-6e8a0d1f3b57",
                      "type": "evacuation_help",
                      "severity": "critical",
                      "status": "unacknowledged",
                      "title": "No puede evacuar: atrapada — EMP-004512",
                      "date_created": "2026-09-24T11:12:30Z",
                      "date_last_modified": "2026-09-24T11:12:30Z",
                      "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",
                        "contractor": "Contrata Ejemplo A"
                      },
                      "device": {
                        "id": "44444444-4444-4444-4444-444444444405",
                        "serial": "OS-W-0005",
                        "battery": 61,
                        "date_last_seen": "2026-09-24T11:12:28Z"
                      },
                      "location": {
                        "latitude": 39.99871,
                        "longitude": -2.99905,
                        "uncertainty_m": 12,
                        "source": "network",
                        "masked": false,
                        "position_date_utc": "2026-09-24T11:12:25Z",
                        "height": null
                      },
                      "location_withheld": false,
                      "details": {
                        "reason": "trapped",
                        "muster_id": "14df4773-88a3-4df5-811e-81cdc8ef12bb"
                      },
                      "nearby": [],
                      "vehicle_warnings": [],
                      "worker_acknowledged_at": null,
                      "acknowledged": null,
                      "resolved": null,
                      "assigned_to": null,
                      "simulated": false,
                      "panel_url": "https://app.safeonuba.com/alertas/5d02c7a9-1b3e-4f80-9c24-6e8a0d1f3b57"
                    }
                  },
                  "low_battery": {
                    "value": {
                      "id": "9e4f1a6b-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
                      "type": "low_battery",
                      "severity": "medium",
                      "status": "unacknowledged",
                      "title": "Batería baja — EMP-002210",
                      "date_created": "2026-09-24T15:20:00Z",
                      "date_last_modified": "2026-09-24T15:20:00Z",
                      "site": {
                        "id": "11111111-1111-1111-1111-111111111111",
                        "name": "Planta Ejemplo — simulación"
                      },
                      "zone": null,
                      "worker": {
                        "id": "33333333-3333-3333-3333-333333333309",
                        "external_id": "EMP-002210",
                        "contractor": "Contrata Ejemplo B"
                      },
                      "device": {
                        "id": "44444444-4444-4444-4444-444444444409",
                        "serial": "OS-W-0009",
                        "battery": 12,
                        "date_last_seen": "2026-09-24T15:19:55Z"
                      },
                      "location": null,
                      "location_withheld": true,
                      "details": {
                        "battery_pct": 12
                      },
                      "nearby": [],
                      "vehicle_warnings": [],
                      "worker_acknowledged_at": null,
                      "acknowledged": null,
                      "resolved": null,
                      "assigned_to": null,
                      "simulated": true,
                      "panel_url": "https://app.safeonuba.com/alertas/9e4f1a6b-2c3d-4e5f-8a9b-0c1d2e3f4a5b"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist, or this identity can’t see it (another site, another contractor).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateAlert",
        "tags": [
          "Alerts"
        ],
        "summary": "Acknowledge or resolve an alert",
        "description": "The alert’s timeline reads “Client · Operator (via integration)”. Acknowledging is not resolving: an acknowledged alert keeps marking the person on the map until it is resolved.\n\n**Scope:** `alerts:write`.",
        "security": [
          {
            "oauth2": [
              "alerts:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Alert id."
            },
            "required": true,
            "description": "Alert id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The alert, now updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist, or this identity can’t see it.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The transition isn’t possible (for example, acknowledging an alert that is already resolved).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The body is not valid.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/musters": {
      "get": {
        "operationId": "listMusters",
        "tags": [
          "Evacuations"
        ],
        "summary": "List evacuations",
        "security": [
          {
            "oauth2": [
              "musters:read"
            ]
          }
        ],
        "description": "**Scope:** `musters:read`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
            },
            "required": false,
            "name": "site_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "completed",
                "cancelled"
              ]
            },
            "required": false,
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-09-24T11:04:12Z"
            },
            "required": false,
            "name": "since",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100,
              "description": "Items per page (maximum 500).",
              "example": 100
            },
            "required": false,
            "description": "Items per page (maximum 500).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` from the previous page."
            },
            "required": false,
            "description": "The `next_cursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Active and past evacuations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MusterPage"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/musters/{id}": {
      "get": {
        "operationId": "getMuster",
        "tags": [
          "Evacuations"
        ],
        "summary": "Status and headcount of an evacuation",
        "description": "The server counts the headcount with the same rule as the dashboard and the closing summary: an accurate position inside a valid point, or a declared arrival at one.\n\n**Scope:** `musters:read`.",
        "security": [
          {
            "oauth2": [
              "musters:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Evacuation id."
            },
            "required": true,
            "description": "Evacuation id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The evacuation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Muster"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist or doesn’t belong to one of the client’s sites.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/musters/{id}/missing": {
      "get": {
        "operationId": "listMissingWorkers",
        "tags": [
          "Evacuations"
        ],
        "summary": "Who is missing",
        "description": "First anyone asking for help or needing rescue, then anyone at a point that is unusable, with no signal, not wearing the watch, and heading out. The last position only with the `positions:read` scope, and if the privacy veil allows it.\n\n**Scope:** `musters:read`.",
        "security": [
          {
            "oauth2": [
              "musters:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Evacuation id."
            },
            "required": true,
            "description": "Evacuation id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Who is missing, by urgency.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MissingWorkerList"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist or doesn’t belong to one of the client’s sites.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workers": {
      "get": {
        "operationId": "listWorkers",
        "tags": [
          "Workers"
        ],
        "summary": "List workers",
        "security": [
          {
            "oauth2": [
              "workers:read"
            ]
          }
        ],
        "description": "**Scope:** `workers:read`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
            },
            "required": false,
            "name": "site_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100,
              "description": "Items per page (maximum 500).",
              "example": 100
            },
            "required": false,
            "description": "Items per page (maximum 500).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` from the previous page."
            },
            "required": false,
            "description": "The `next_cursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Workers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkerPage"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workers/{id}": {
      "get": {
        "operationId": "getWorker",
        "tags": [
          "Workers"
        ],
        "summary": "Worker detail",
        "security": [
          {
            "oauth2": [
              "workers:read"
            ]
          }
        ],
        "description": "**Scope:** `workers:read`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Worker id."
            },
            "required": true,
            "description": "Worker id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The worker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Worker"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist or this identity can’t see it.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices": {
      "get": {
        "operationId": "listDevices",
        "tags": [
          "Workers"
        ],
        "summary": "List watches",
        "security": [
          {
            "oauth2": [
              "workers:read"
            ]
          }
        ],
        "description": "**Scope:** `workers:read`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
            },
            "required": false,
            "name": "site_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100,
              "description": "Items per page (maximum 500).",
              "example": 100
            },
            "required": false,
            "description": "Items per page (maximum 500).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` from the previous page."
            },
            "required": false,
            "description": "The `next_cursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Watches: battery, whether it is being worn, last connection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DevicePage"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/positions/latest": {
      "get": {
        "operationId": "listLatestPositions",
        "tags": [
          "Positions"
        ],
        "summary": "Latest visible positions",
        "description": "Only those the privacy veil allows right now. Every position served with an alert behind it is recorded on the alert (“Who has seen their position”).\n\n**Scope:** `positions:read`.",
        "security": [
          {
            "oauth2": [
              "positions:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
            },
            "required": false,
            "name": "site_id",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Positions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PositionList"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "listEvents",
        "tags": [
          "Events"
        ],
        "summary": "Replay events",
        "description": "The same ones that arrive by webhook, kept for 30 days: to recover what was lost after an outage on your receiver.\n\n**Scope:** any.",
        "security": [
          {
            "oauth2": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-09-24T11:04:12Z",
              "description": "Since when (at most 30 days back)."
            },
            "required": true,
            "description": "Since when (at most 30 days back).",
            "name": "since",
            "in": "query"
          },
          {
            "schema": {
              "$ref": "#/components/schemas/EventType"
            },
            "required": false,
            "description": "`muster.updated`: the headcount has changed (checked every 5 s, only emitted if it changes). `muster.targets_changed`: the control room has ruled out or restored a muster point.",
            "name": "type",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100,
              "description": "Items per page (maximum 500).",
              "example": 100
            },
            "required": false,
            "description": "Items per page (maximum 500).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` from the previous page."
            },
            "required": false,
            "description": "The `next_cursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Events, from the oldest to the most recent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventPage"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "List destinations",
        "security": [
          {
            "oauth2": [
              "webhooks:manage"
            ]
          }
        ],
        "description": "**Scope:** `webhooks:manage`.",
        "responses": {
          "200": {
            "description": "Destinations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointList"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Create a destination",
        "security": [
          {
            "oauth2": [
              "webhooks:manage"
            ]
          }
        ],
        "description": "**Scope:** `webhooks:manage`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Destination created, with its signing secret (this once only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointCreated"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The body is not valid (the URL must be https).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}": {
      "delete": {
        "operationId": "deleteWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a destination",
        "security": [
          {
            "oauth2": [
              "webhooks:manage"
            ]
          }
        ],
        "description": "**Scope:** `webhooks:manage`.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Destination id."
            },
            "required": true,
            "description": "Destination id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/rotate-secret": {
      "post": {
        "operationId": "rotateWebhookSecret",
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate the signing secret",
        "description": "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.\n\n**Scope:** `webhooks:manage`.",
        "security": [
          {
            "oauth2": [
              "webhooks:manage"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Destination id."
            },
            "required": true,
            "description": "Destination id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The new secret (this once only) and until when the previous one signs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretRotated"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/test": {
      "post": {
        "operationId": "testWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a test event",
        "description": "Sends an `alert.created` marked `simulated: true`, signed like a real one.\n\n**Scope:** `webhooks:manage`.",
        "security": [
          {
            "oauth2": [
              "webhooks:manage"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
              "description": "Destination id."
            },
            "required": true,
            "description": "Destination id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "202": {
            "description": "Queued: the result shows on the destination (`failing_since`)."
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "It doesn’t exist.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/scenarios": {
      "post": {
        "operationId": "runSandboxScenario",
        "tags": [
          "Sandbox"
        ],
        "summary": "Launch a test scenario",
        "description": "**Only on `sandbox.api.safeonuba.com`**; in production, 404. A scenario doesn’t invent alerts: simulated watches at a sandbox site send their facts and the usual engine decides, so what arrives —webhook, `GET`, headcount— is the same as would arrive from a real watch, marked `simulated: true`. At most five open at once per client and one evacuation per site.\n\n**Scope:** any.",
        "security": [
          {
            "oauth2": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SandboxScenarioRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Launched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxScenarioRun"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, has expired or has been revoked.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope the route requires, or the site doesn’t belong to the client.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "This is not the sandbox, or the site isn’t yours.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "An evacuation is already under way at that site, or five scenarios are open.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The site isn’t a sandbox site, or it has a real watch and nothing runs.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "alert.*": {
      "post": {
        "operationId": "alertEvent",
        "security": [],
        "tags": [
          "Webhooks"
        ],
        "summary": "Changes to an alert",
        "description": "One `POST` per event to each subscribed destination, with the complete object.\n\nHeaders: `X-SafeOnuba-Event-Id`, `X-SafeOnuba-Timestamp` (Unix seconds) and\n`X-SafeOnuba-Signature: v1=<hex(HMAC_SHA256(secret, timestamp + \".\" + body))>`. Reject\nanything more than 5 min out of sync. During a rotation two signatures arrive, separated by a comma.\n\nAcknowledgement: any `2xx` in under 5 s. Retries at 10 s, 30 s, 2 min, 10 min, 30 min,\n1 h and then every hour up to 24 h. “At least once” delivery (deduplicate by `id`) and **no guaranteed\norder** (use `date_last_modified`). SOS, falls, “I can’t evacuate” and evacuations go out\nbefore the rest.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received. Any `2xx` will do."
          }
        }
      }
    },
    "muster.*": {
      "post": {
        "operationId": "musterEvent",
        "security": [],
        "tags": [
          "Webhooks"
        ],
        "summary": "Changes to an evacuation",
        "description": "One `POST` per event to each subscribed destination, with the complete object.\n\nHeaders: `X-SafeOnuba-Event-Id`, `X-SafeOnuba-Timestamp` (Unix seconds) and\n`X-SafeOnuba-Signature: v1=<hex(HMAC_SHA256(secret, timestamp + \".\" + body))>`. Reject\nanything more than 5 min out of sync. During a rotation two signatures arrive, separated by a comma.\n\nAcknowledgement: any `2xx` in under 5 s. Retries at 10 s, 30 s, 2 min, 10 min, 30 min,\n1 h and then every hour up to 24 h. “At least once” delivery (deduplicate by `id`) and **no guaranteed\norder** (use `date_last_modified`). SOS, falls, “I can’t evacuate” and evacuations go out\nbefore the rest.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received. Any `2xx` will do."
          }
        }
      }
    }
  }
}