Empezar
Autenticación y scopes
OAuth2 client_credentials: cómo pedir un token, qué scopes existen, cómo rotar el client_secret sin cortes y qué errores devuelve el endpoint del token.
En esta página
La API usa OAuth2 con el flujo client_credentials: tu servidor cambia sus credenciales por un token de una hora y manda ese token en cada llamada.
Credenciales
Cada cliente se da de alta a mano, con un motivo que queda registrado. Al darte de alta recibes:
- un
client_id, que identifica a tu empresa; - un
client_secret, que empieza porcs_y se enseña una sola vez.
Un cliente puede tener dos secretos activos a la vez. Es lo que permite rotarlo sin cortar la integración (ver Rotar el secreto).
El client_secret vive en tu servidor, en un gestor de secretos o una variable de entorno. Nunca en un navegador, una aplicación de escritorio o un repositorio.
Pedir un token
POST /v1/oauth/token, con el cuerpo en application/x-www-form-urlencoded:
| Campo | Obligatorio | Valor |
|---|---|---|
grant_type | Sí | client_credentials |
client_id | Sí | Tu identificador de cliente. |
client_secret | Sí | Tu secreto. |
scope | No | Scopes separados por espacios, entre los que tiene tu cliente. Sin él, el token lleva todos los de tu cliente. |
curl -X POST https://sandbox.api.safeonuba.com/v1/oauth/token \
-d grant_type=client_credentials \
-d client_id="$SAFEONUBA_CLIENT_ID" \
-d client_secret="$SAFEONUBA_CLIENT_SECRET" \
-d scope="alerts:read musters:read"const res = await fetch('https://sandbox.api.safeonuba.com/v1/oauth/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SAFEONUBA_CLIENT_ID,
client_secret: process.env.SAFEONUBA_CLIENT_SECRET,
scope: 'alerts:read musters:read',
}),
});
const token = await res.json();import os
import requests
res = requests.post(
"https://sandbox.api.safeonuba.com/v1/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["SAFEONUBA_CLIENT_ID"],
"client_secret": os.environ["SAFEONUBA_CLIENT_SECRET"],
"scope": "alerts:read musters:read",
},
timeout=10,
)
res.raise_for_status()
token = res.json()using System.Net.Http.Json;
using System.Text.Json;
using var http = new HttpClient();
var res = await http.PostAsync("https://sandbox.api.safeonuba.com/v1/oauth/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = Environment.GetEnvironmentVariable("SAFEONUBA_CLIENT_ID")!,
["client_secret"] = Environment.GetEnvironmentVariable("SAFEONUBA_CLIENT_SECRET")!,
["scope"] = "alerts:read musters:read",
}));
res.EnsureSuccessStatusCode();
var token = await res.Content.ReadFromJsonAsync<JsonElement>();La respuesta:
| Campo | Qué es |
|---|---|
access_token | El token. Es opaco: no intentes leerlo, solo reenvíalo. |
token_type | Siempre Bearer. |
expires_in | Segundos de validez: 3600. |
scope | Los scopes que lleva el token, separados por espacios. |
El token se revoca al instante si se da de baja al cliente.
Usar el token
En todas las demás llamadas, en la cabecera Authorization:
GET /v1/alerts?status=unacknowledged HTTP/1.1
Host: sandbox.api.safeonuba.com
Authorization: Bearer <token>Renovarlo
client_credentials no tiene token de refresco: cuando uno caduca se pide otro con las mismas credenciales. Pide uno, guárdalo en memoria y renuévalo un poco antes de que caduque. No pidas un token por cada llamada: el endpoint del token tiene su propio límite por IP.
let token = null;
let caduca = 0;
export async function tokenSafeOnuba() {
// Se renueva un minuto antes de caducar.
if (token && Date.now() < caduca - 60_000) return token;
const res = await fetch('https://sandbox.api.safeonuba.com/v1/oauth/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SAFEONUBA_CLIENT_ID,
client_secret: process.env.SAFEONUBA_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`token: ${res.status} ${await res.text()}`);
const datos = await res.json();
token = datos.access_token;
caduca = Date.now() + datos.expires_in * 1000;
return token;
}import os
import time
import requests
_token = None
_caduca = 0.0
def token_safeonuba() -> str:
global _token, _caduca
# Se renueva un minuto antes de caducar.
if _token and time.time() < _caduca - 60:
return _token
res = requests.post(
"https://sandbox.api.safeonuba.com/v1/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["SAFEONUBA_CLIENT_ID"],
"client_secret": os.environ["SAFEONUBA_CLIENT_SECRET"],
},
timeout=10,
)
res.raise_for_status()
datos = res.json()
_token = datos["access_token"]
_caduca = time.time() + datos["expires_in"]
return _tokenusing System.Net.Http.Json;
using System.Text.Json;
public sealed class TokenSafeOnuba(HttpClient http)
{
private readonly SemaphoreSlim _cerrojo = new(1, 1);
private string? _token;
private DateTimeOffset _caduca;
public async Task<string> ObtenerAsync()
{
await _cerrojo.WaitAsync();
try
{
// Se renueva un minuto antes de caducar.
if (_token is not null && DateTimeOffset.UtcNow < _caduca.AddMinutes(-1)) return _token;
var res = await http.PostAsync("https://sandbox.api.safeonuba.com/v1/oauth/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = Environment.GetEnvironmentVariable("SAFEONUBA_CLIENT_ID")!,
["client_secret"] = Environment.GetEnvironmentVariable("SAFEONUBA_CLIENT_SECRET")!,
}));
res.EnsureSuccessStatusCode();
var datos = await res.Content.ReadFromJsonAsync<JsonElement>();
_token = datos.GetProperty("access_token").GetString()!;
_caduca = DateTimeOffset.UtcNow.AddSeconds(datos.GetProperty("expires_in").GetDouble());
return _token;
}
finally
{
_cerrojo.Release();
}
}
}Si una llamada devuelve 401 con un token que creías vigente (por ejemplo, porque se ha revocado), descarta el que tienes y pide otro una vez. Si vuelve a fallar, no reintentes en bucle: revisa las credenciales.
Scopes
Cada token lleva los scopes que tu cliente tiene concedidos, o el subconjunto que pidas en scope. Pide solo los que usa cada servicio: el receptor de webhooks, por ejemplo, no necesita ninguno, porque no llama a la API.
| Scope | Para qué | Operaciones |
|---|---|---|
alerts:read | Leer alertas y zonas. | |
alerts:write | Reconocer y cerrar alertas (siempre con el operador). | |
positions:read | Posiciones que el velo deja ver. | |
musters:read | Evacuaciones y recuento. | |
workers:read | Trabajadores y relojes. | |
webhooks:manage | Destinos de webhook. |
GET /v1/sites, GET /v1/events y POST /v1/sandbox/scenarios valen con cualquier token válido.
alerts:write siempre actúa en nombre de una persona: el cuerpo de PUT /v1/alerts/{id} exige el actor, el operador de tu sala que reconoce o cierra la alerta.
Rotar el client_secret
Como un cliente puede tener dos secretos activos, la rotación no corta nada:
- Pide un secreto nuevo por el mismo canal por el que pediste el acceso.
- Despliega el nuevo en tus servicios. Mientras tanto, los que aún usan el antiguo siguen funcionando.
- Comprueba que todos los tokens se piden ya con el nuevo.
- Pide que se retire el antiguo.
El secreto de firma de los webhooks es otra cosa y se rota por la API: ver Verificar la firma.
Errores del token
El endpoint del token responde en el formato de OAuth2 (RFC 6749 §5.2), no en problem+json: un campo error y, a veces, error_description. Responde 401 si el cliente o el secreto no son correctos, y 400 para lo demás.
| error | Qué significa |
|---|---|
invalid_request | Falta un campo obligatorio o la petición está mal formada. |
invalid_client | El client_id o el client_secret no son correctos, o el cliente se ha dado de baja. |
unauthorized_client | El cliente no está autorizado a usar este tipo de concesión. |
unsupported_grant_type | grant_type no es client_credentials. |
invalid_scope | Se ha pedido un scope que no existe o que el cliente no tiene. |
En el resto de la API, un token que falta, ha caducado o se ha revocado da 401, y un token sin el scope que pide la ruta da 403. Ver Errores.