# Sandbox: integrating without watches

> Run SOS, fall, heat stress or full evacuation scenarios with simulated watches and receive exactly what would arrive from a real plant.

Page: https://safeonuba.com/en/developers/sandbox · SafeOnuba API v1 (`api_version: "2026-09-24"`), in preview: the contract may change.

The sandbox (`https://sandbox.api.safeonuba.com`) is a complete environment with **simulated watches**. It’s for integrating without having watches, without being at the plant and without bothering anyone.

A scenario doesn’t invent alerts: the simulated watches at a sandbox site send their facts and the usual engine decides. What reaches you —webhook, `GET`, headcount— is the same as would come from a real watch, with one difference: everything carries `simulated: true`.

## Launching a scenario

`POST /v1/sandbox/scenarios`, with any valid token:

| Field | Required | What it is |
| --- | --- | --- |
| `scenario` | Yes | `sos`, `fall`, `heat_stress` or `evacuation`. |
| `site_id` | No | The sandbox site. Without it, the first of yours that is a sandbox site. |

```bash
curl -X POST https://sandbox.api.safeonuba.com/v1/sandbox/scenarios \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "scenario": "evacuation" }'
```

```javascript
const res = await fetch('https://sandbox.api.safeonuba.com/v1/sandbox/scenarios', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${await safeOnubaToken()}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ scenario: 'evacuation' }),
});
const run = await res.json(); // { run_id, scenario, site_id, resource, ends_at }
```

```python
res = requests.post(
    "https://sandbox.api.safeonuba.com/v1/sandbox/scenarios",
    headers={"Authorization": f"Bearer {safeonuba_token()}"},
    json={"scenario": "evacuation"},
    timeout=10,
)
res.raise_for_status()
run = res.json()  # run_id, scenario, site_id, resource, ends_at
```

```csharp
var res = await http.PostAsync("https://sandbox.api.safeonuba.com/v1/sandbox/scenarios",
    JsonContent.Create(new { scenario = "evacuation" }));
res.EnsureSuccessStatusCode();
var run = await res.Content.ReadFromJsonAsync<JsonElement>(); // run_id, scenario, site_id, resource, ends_at
```

The response (`202`) says what it has opened (`resource`: an alert or an evacuation, with its `id`) and when it closes by itself (`ends_at`). Query it with its `GET` or wait for the webhook.

## Alert scenarios

| Scenario | What happens |
| --- | --- |
| `sos` | A test person presses SOS. |
| `fall` | A test person falls. |
| `heat_stress` | A test person suffers heat stress. |

The alert appears (`alert.created`). You can acknowledge and close it from your control room with `PUT /v1/alerts/{id}`; if nobody closes it first, **it closes itself after 10 minutes** (`alert.resolved`).

## A complete evacuation

`evacuation` is a general **3-minute** evacuation that runs through the whole cycle, designed for testing your control room’s banner from start to finish:

| Moment | What happens | What reaches you |
| --- | --- | --- |
| 0 s | The evacuation starts. | `muster.started` |
| 15 s | Four people reach point A. | `muster.updated` |
| 35 s | Someone reaches point B and the control room rules it out. | `muster.targets_changed` |
| 55 s | A person asks for help. | `alert.created` of type `evacuation_help` |
| 95 s | The person who was at B goes to C. | `muster.updated` |
| 3 min | All clear. | `muster.ended` |

In between, a `muster.updated` arrives every time the headcount changes.

With this you can check what is hardest to test at a real plant: that your control room shows a ruled-out point correctly, that the person asking for help moves to the top of “who’s missing”, and that the banner closes when it ends.

## Sandbox limits

- At most **5 scenarios open at once** per client.
- **One evacuation per site** at a time.
- Going over either gives `409`.
- In production, `POST /v1/sandbox/scenarios` responds `404`: it only exists in the sandbox.
- A site that isn’t a sandbox site, or that has a real watch, responds `422` and runs nothing.

## Telling what is simulated

Everything that comes out of a scenario carries `simulated: true`: the alerts, and whatever is generated from them. Events from `POST /v1/webhooks/{id}/test` also carry `organization_id: null`.

If your control room shares a database or screens with production, filter on `simulated` so that a test is never mistaken for a real alert.
