# Sandbox: integrar sin relojes

> Lanza escenarios de SOS, caída, estrés térmico o una evacuación completa con relojes simulados y recibe lo mismo que llegaría de una planta real.

Página: https://safeonuba.com/desarrolladores/sandbox · API de SafeOnuba v1 (`api_version: "2026-09-24"`), en vista previa: el contrato puede cambiar.

El sandbox (`https://sandbox.api.safeonuba.com`) es un entorno completo con **relojes simulados**. Sirve para integrar sin tener relojes, sin estar en la planta y sin molestar a nadie.

Un escenario no se inventa alertas: los relojes simulados de un centro de sandbox mandan sus hechos y el motor de siempre decide. Lo que te llega —webhook, `GET`, recuento— es lo mismo que llegaría de un reloj de verdad, con una diferencia: todo lleva `simulated: true`.

## Lanzar un escenario

`POST /v1/sandbox/scenarios`, con cualquier token válido:

| Campo | Obligatorio | Qué es |
| --- | --- | --- |
| `scenario` | Sí | `sos`, `fall`, `heat_stress` o `evacuation`. |
| `site_id` | No | El centro de sandbox. Sin él, el primero de los tuyos que sea de sandbox. |

```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 tokenSafeOnuba()}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ scenario: 'evacuation' }),
});
const ejecucion = 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 {token_safeonuba()}"},
    json={"scenario": "evacuation"},
    timeout=10,
)
res.raise_for_status()
ejecucion = 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 ejecucion = await res.Content.ReadFromJsonAsync<JsonElement>(); // run_id, scenario, site_id, resource, ends_at
```

La respuesta (`202`) dice qué ha abierto (`resource`: una alerta o una evacuación, con su `id`) y cuándo se cierra solo (`ends_at`). Consúltalo con su `GET` o espera al webhook.

## Escenarios de alerta

| Escenario | Qué pasa |
| --- | --- |
| `sos` | Una persona de prueba pulsa el SOS. |
| `fall` | Una persona de prueba se cae. |
| `heat_stress` | Una persona de prueba sufre estrés térmico. |

Sale la alerta (`alert.created`). Puedes reconocerla y cerrarla desde tu sala con `PUT /v1/alerts/{id}`; si nadie la cierra antes, **se cierra sola a los 10 minutos** (`alert.resolved`).

## Evacuación completa

`evacuation` es una evacuación general de **3 minutos** que recorre el ciclo entero, pensada para probar el banner de tu sala de principio a fin:

| Momento | Qué pasa | Qué te llega |
| --- | --- | --- |
| 0 s | Empieza la evacuación. | `muster.started` |
| 15 s | Llegan cuatro personas al punto A. | `muster.updated` |
| 35 s | Alguien llega al punto B y la sala lo descarta. | `muster.targets_changed` |
| 55 s | Una persona pide ayuda. | `alert.created` de tipo `evacuation_help` |
| 95 s | Quien estaba en B se va al C. | `muster.updated` |
| 3 min | Todo despejado. | `muster.ended` |

Entre medias llega un `muster.updated` cada vez que cambia el recuento.

Con esto puedes comprobar lo que más cuesta probar en una planta real: que tu sala pinta bien un punto descartado, que la persona que pide ayuda sube arriba en «quién falta» y que el banner se cierra al terminar.

## Límites del sandbox

- Como mucho **5 escenarios abiertos a la vez** por cliente.
- **Una evacuación por centro** a la vez.
- Pasarse de cualquiera de los dos da `409`.
- En producción, `POST /v1/sandbox/scenarios` responde `404`: solo existe en el sandbox.
- Un centro que no es de sandbox, o que tiene un reloj de verdad, responde `422` y no corre nada.

## Distinguir lo simulado

Todo lo que sale de un escenario lleva `simulated: true`: las alertas, y lo que se genera a partir de ellas. Los eventos de `POST /v1/webhooks/{id}/test` llevan además `organization_id: null`.

Si tu sala comparte base de datos o pantallas con producción, filtra por `simulated` para que una prueba no se confunda nunca con una alerta real.
