v1 · preview

Try webhook signature, alert.created, location_withheld or 429.

↑ ↓ moveEnter openEsc close

Get started

Authentication and scopes

OAuth2 client_credentials: how to request a token, which scopes exist, how to rotate the client_secret without downtime and which errors the token endpoint returns.

On this page

The API uses OAuth2 with the client_credentials flow: your server exchanges its credentials for a one-hour token and sends that token on every call.

Credentials

Each customer is registered by hand, with a reason that is recorded. When you are registered you receive:

  • a client_id, which identifies your company;
  • a client_secret, which starts with cs_ and is shown only once.

A client can have two active secrets at the same time. That is what lets you rotate it without cutting the integration (see Rotating the client_secret).

The client_secret lives on your server, in a secrets manager or an environment variable. Never in a browser, a desktop application or a repository.

Requesting a token

POST /v1/oauth/token, with the body as application/x-www-form-urlencoded:

FieldRequiredValue
grant_typeYesclient_credentials
client_idYesYour client identifier.
client_secretYesYour secret.
scopeNoSpace-separated scopes, from among those your client has. Without it, the token carries all of your client’s scopes.
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"

The response:

FieldWhat it is
access_tokenThe token. It is opaque: don’t try to read it, just send it back.
token_typeAlways Bearer.
expires_inSeconds of validity: 3600.
scopeThe scopes the token carries, separated by spaces.

The token is revoked instantly if the client is deregistered.

Using the token

On every other call, in the Authorization header:

HTTP
GET /v1/alerts?status=unacknowledged HTTP/1.1
Host: sandbox.api.safeonuba.com
Authorization: Bearer <token>

Renewing it

client_credentials has no refresh token: when one expires you request another with the same credentials. Request one, keep it in memory and renew it a little before it expires. Don’t request a token for every call: the token endpoint has its own limit per IP.

let token = null;
let expiresAt = 0;

export async function safeOnubaToken() {
  // Renewed a minute before it expires.
  if (token && Date.now() < expiresAt - 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 data = await res.json();
  token = data.access_token;
  expiresAt = Date.now() + data.expires_in * 1000;
  return token;
}

If a call returns 401 with a token you thought was valid (because it has been revoked, for example), discard the one you have and request another, once. If it fails again, don’t retry in a loop: check the credentials.

Scopes

Each token carries the scopes your client has been granted, or the subset you ask for in scope. Ask only for those each service uses: the webhook receiver, for example, needs none, because it doesn’t call the API.

ScopeWhat forOperations
alerts:readRead alerts and zones.
alerts:writeAcknowledge and resolve alerts (always with the operator).
positions:readPositions the privacy veil allows.
musters:readEvacuations and headcount.
workers:readWorkers and watches.
webhooks:manageWebhook destinations.

GET /v1/sites, GET /v1/events and POST /v1/sandbox/scenarios work with any valid token.

alerts:write always acts on behalf of a person: the body of PUT /v1/alerts/{id} requires the actor, the operator in your control room who acknowledges or closes the alert.

Rotating the client_secret

Because a client can have two active secrets, rotation cuts nothing:

  1. Request a new secret through the same channel you used to request access.
  2. Deploy the new one to your services. In the meantime, those still using the old one keep working.
  3. Check that all tokens are now requested with the new one.
  4. Ask for the old one to be withdrawn.

The webhook signing secret is a different thing and is rotated through the API: see Verifying a webhook signature.

Token errors

The token endpoint responds in the OAuth2 format (RFC 6749 §5.2), not in problem+json: an error field and, sometimes, error_description. It responds 401 if the client or the secret are wrong, and 400 for everything else.

errorWhat it means
invalid_requestA required field is missing or the request is malformed.
invalid_clientThe client_id or the client_secret is wrong, or the client has been deregistered.
unauthorized_clientThe client is not authorised to use this grant type.
unsupported_grant_typegrant_type is not client_credentials.
invalid_scopeA scope was requested that does not exist or that the client does not have.

On the rest of the API, a token that is missing, has expired or has been revoked gives 401, and a token without the scope the path needs gives 403. See Errors.