v1 · vista previa

Prueba con webhook firma, alert.created, location_withheld o 429.

↑ ↓ moverseIntro abrirEsc cerrar

Tiempo real

Webhooks

Crea un destino, entiende el formato de cada envío, responde a tiempo, deduplica, filtra por centro o gravedad y envía un evento de prueba.

En esta página

Con un webhook, SafeOnuba avisa a tu servidor en cuanto pasa algo: una alerta nueva, un operador que se hace cargo, un cambio en el recuento de una evacuación. Tu sala no tiene que preguntar, y la alerta le llega en uno o dos segundos.

Crear un destino

Un destino es una URL tuya y los eventos que quieres recibir en ella. Se crea con POST /v1/webhooks y el scope webhooks:manage.

CampoObligatorioQué es
urlSíTu receptor. Solo https, y en una dirección pública.
event_typesSíLos eventos: grupos enteros (alert.*, muster.*) o tipos concretos (alert.created).
filtersNoQué parte de esos eventos: por centro, gravedad o contrata. Ver Filtros.
curl -X POST https://sandbox.api.safeonuba.com/v1/webhooks \
  -H "Authorization: Bearer $SAFEONUBA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://sala.cliente.example/safeonuba",
    "event_types": ["alert.*", "muster.*"],
    "filters": { "min_severity": "high" }
  }'

La respuesta es el destino con un campo más, secret, que empieza por whsec_. Solo se enseña esta vez. Es el secreto con el que verificas la firma de cada envío.

Formato de un envío

Cada evento es un POST a tu URL con el cuerpo en JSON y estas cabeceras:

CabeceraQué lleva
X-SafeOnuba-Event-IdEl id del evento.
X-SafeOnuba-TimestampCuándo se firmó, en segundos Unix.
X-SafeOnuba-Signaturev1=<hex>. Durante una rotación del secreto, dos firmas separadas por coma.
User-AgentSafeOnuba-Webhooks/2026-09-24

El cuerpo es siempre el mismo sobre:

CampoQué es
idIdentificador del evento. Es el que usas para deduplicar.
typeEl tipo de evento (ver Eventos).
api_version"2026-09-24".
created_atCuándo ocurrió, en ISO 8601 UTC.
organization_idLa empresa del centro. null en los eventos de prueba.
site_idEl centro.
data{ "alert": … } o { "muster": … }: el objeto completo, tal como lo devolvería el GET en ese instante.
alert.created
{
  "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "type": "alert.created",
  "api_version": "2026-09-24",
  "created_at": "2026-09-24T11:04:12Z",
  "organization_id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "site_id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
  "data": {
    "alert": {
      "id": "0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90",
      "type": "sos",
      "severity": "critical",
      "status": "unacknowledged",
      "title": "SOS activado — Ana Ejemplo",
      "date_created": "2026-09-24T11:04:12Z",
      "date_last_modified": "2026-09-24T11:04:12Z",
      "site": {
        "id": "11111111-1111-1111-1111-111111111111",
        "name": "Planta Ejemplo — simulación"
      },
      "zone": null,
      "worker": {
        "id": "33333333-3333-3333-3333-333333333305",
        "external_id": "EMP-004512",
        "name": "Ana Ejemplo",
        "contractor": "Contrata Ejemplo A"
      },
      "device": {
        "id": "44444444-4444-4444-4444-444444444405",
        "serial": "OS-W-0005",
        "battery": 64,
        "date_last_seen": "2026-09-24T11:04:10Z"
      },
      "location": {
        "latitude": 40.00121,
        "longitude": -3.00214,
        "uncertainty_m": 8,
        "source": "gps",
        "masked": false,
        "position_date_utc": "2026-09-24T11:04:08Z",
        "height": null
      },
      "location_withheld": false,
      "details": {
        "trigger": "watch_button"
      },
      "nearby": [
        {
          "kind": "vehicle",
          "worker": {
            "id": "33333333-3333-3333-3333-333333333309",
            "external_id": "EMP-002210",
            "name": "Carlos Ejemplo",
            "contractor": "Contrata Ejemplo B"
          },
          "vehicle_type": "Grúa móvil",
          "distance_m": 18,
          "reported_at": "2026-09-24T11:04:02Z"
        },
        {
          "kind": "person",
          "worker": {
            "id": "33333333-3333-3333-3333-333333333301",
            "external_id": "EMP-000981",
            "name": "Diego Ejemplo",
            "contractor": null
          },
          "vehicle_type": null,
          "distance_m": 26,
          "reported_at": "2026-09-24T11:03:58Z"
        }
      ],
      "vehicle_warnings": [],
      "worker_acknowledged_at": null,
      "acknowledged": null,
      "resolved": null,
      "assigned_to": null,
      "simulated": false,
      "panel_url": "https://app.safeonuba.com/alertas/0f6c2b7e-3a1d-4c55-9b0e-7d2f4a8c1e90"
    }
  }
}

Como data trae el objeto entero, normalmente no hace falta llamar a la API al recibir un evento: tienes todo lo que tu sala necesita para pintarlo.

Responder a tiempo

Un envío cuenta como entregado cuando tu receptor responde cualquier 2xx en menos de 5 segundos. Las redirecciones no se siguen: un 301 o un 302 es un fallo.

Por eso tu receptor debe hacer lo mínimo: verificar la firma, dejar el evento en una cola (una tabla, Redis, RabbitMQ, lo que ya uses) y responder. Procesa después, fuera de la petición. Si tu sistema de sala tarda o se cae, la cola aguanta y SafeOnuba no reintenta en balde.

Reintentos

Si tu receptor no responde 2xx a tiempo, el envío se reintenta a los 10 s, 30 s, 2 min, 10 min, 30 min y 1 h, y después cada hora hasta 24 horas desde el evento.

Si una entrega se da por perdida, avisamos por correo al contacto técnico de tu empresa. El evento sigue disponible en GET /v1/events durante 30 días (ver Recuperar lo perdido).

Un destino que falla no se desactiva solo: si lo hiciera, dejaría de llegar el siguiente SOS sin que nadie lo hubiera decidido. En su lugar, failing_since dice desde cuándo lleva fallando. Vigílalo con GET /v1/webhooks.

Duplicados

La entrega es «al menos una vez»: un mismo evento puede llegar más de una vez, por ejemplo si tu receptor lo procesó pero la respuesta no llegó a tiempo. Deduplica por el id del evento (también viene en X-SafeOnuba-Event-Id).

La forma más sencilla es una tabla con el id como clave:

SQL
CREATE TABLE eventos_safeonuba (
  id          uuid PRIMARY KEY,
  recibido_en timestamptz NOT NULL DEFAULT now()
);

-- Devuelve una fila si el evento es nuevo, ninguna si ya se había recibido.
INSERT INTO eventos_safeonuba (id) VALUES ($1)
ON CONFLICT (id) DO NOTHING
RETURNING id;

Orden

El orden de entrega no está garantizado. Lo urgente sale primero: SOS, caídas, «no puedo evacuar» y evacuaciones adelantan al resto. Y un reintento puede llegar después de un evento más nuevo.

Cada evento trae el objeto completo, así que la regla es quedarse con el más reciente y descartar lo que llegue más viejo:

  • Para ordenar los eventos entre sí, usa su created_at.
  • Para una alerta, compara date_last_modified: si lo que llega es más antiguo que lo que ya tienes, descártalo.
  • Si detectas un hueco, por ejemplo tras un corte, rellénalo con GET /v1/events.
Node
const ultimaVersion = new Map(); // id de la alerta → date_last_modified

function aplicarAlerta(alerta) {
  const conocida = ultimaVersion.get(alerta.id);
  if (conocida && Date.parse(conocida) >= Date.parse(alerta.date_last_modified)) return; // más vieja
  ultimaVersion.set(alerta.id, alerta.date_last_modified);
  pintarEnSala(alerta);
}

Filtros

Con filters recibes solo una parte de los eventos que has pedido:

FiltroQué hace
site_idsSolo esos centros.
min_severitySolo alertas de esa gravedad o más (low, medium, high, critical).
contractorsSolo lo que afecta a esas contratas.

Puedes tener varios destinos con filtros distintos: por ejemplo, las alertas críticas de todas las plantas a la sala central y todo lo de una planta a su propia sala.

Enviar uno de prueba

POST /v1/webhooks/{id}/test manda a ese destino un alert.created marcado simulated: true, firmado como uno real. Responde 202 al ponerlo en cola; el resultado se ve en el destino, en failing_since.

Para probar el recorrido completo, con alertas y evacuaciones de verdad hechas por relojes simulados, usa el sandbox.

Rotar el secreto

POST /v1/webhooks/{id}/rotate-secret da un secreto nuevo. El anterior sigue firmando 24 horas, y durante ese tiempo cada envío lleva las dos firmas. Cómo cambiar de secreto sin perder ningún evento: Verificar la firma.

Gestionar los destinos

Webhooks o consultas

Usa webhooks para enterarte de lo que pasa, y la API para lo demás: cargar el estado al arrancar, la lista de quién falta, reconocer una alerta. Consultar en bucle llega más tarde que un webhook y gasta tus límites de uso.