Authentication

Every /api/ request needs an access token. You get one by exchanging your QAirbon username and password for a token, then sending that token on each request. Tokens last one hour.

Get a token

Post your username and password to the token endpoint. Replace {{USER_NAME}} and {{PASSWORD}} with your own credentials. No client secret is needed.

cURL

curl --location 'https://keycloak.qairbon.com/realms/QairbonAPIDev/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=auth' \
--data-urlencode 'username={{USER_NAME}}' \
--data-urlencode 'password={{PASSWORD}}'

Python

import requests

TOKEN_URL = "https://keycloak.qairbon.com/realms/QairbonAPIDev/protocol/openid-connect/token"

response = requests.post(TOKEN_URL, data={
    "grant_type": "password",
    "client_id": "auth",
    "username": "you@example.com",
    "password": "your-password",
})
response.raise_for_status()
token = response.json()["access_token"]

JavaScript

const TOKEN_URL =
  "https://keycloak.qairbon.com/realms/QairbonAPIDev/protocol/openid-connect/token";

const response = await fetch(TOKEN_URL, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "password",
    client_id: "auth",
    username: "you@example.com",
    password: "your-password",
  }),
});
const { access_token } = await response.json();

Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Use the token

Send it as a Bearer token in the Authorization header of every API request.

cURL

curl -H "Authorization: Bearer $TOKEN" \
     https://${API_HOST}/api/${API_VERSION}/collections/kpi/items

Python

headers = {"Authorization": f"Bearer {token}"}

data = requests.get(
    "https://${API_HOST}/api/${API_VERSION}/collections/kpi/items",
    headers=headers,
).json()

JavaScript

const data = await fetch(
  "https://${API_HOST}/api/${API_VERSION}/collections/kpi/items",
  { headers: { Authorization: `Bearer ${access_token}` } }
).then(r => r.json());

Refresh the token

An access token expires after one hour. The token response also gives you a refresh_token, which buys a new access token without sending your password again.

curl --location 'https://keycloak.qairbon.com/realms/QairbonAPIDev/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'client_id=auth' \
--data-urlencode 'refresh_token={{REFRESH_TOKEN}}'

The response has the same shape as the one above, with a fresh access_token and a new refresh_token. If the refresh token has itself expired, ask for a token with your password again.

What your role changes

The token carries your role, and the API answers accordingly: you only ever see the sites your account is entitled to, and some figures are counted differently.

RoleWhat it sees
userThe validated sites of its own portfolio
ownerThe same, with extended management rights on its own sites
validatorAlso the sites still awaiting validation, which it reviews
adminEverything, including unvalidated sites and unpublished alerts
i
Two accounts querying the same endpoint can legitimately get different numbers. A site awaiting validation is listed for everyone but carries no measurements, and it is counted in kpi:site_count only for validator and admin.

Endpoints that need no token

The documentation endpoints are open:

EndpointDescription
/api/${API_VERSION}/docsSwagger UI
/api/${API_VERSION}/redocReDoc reference
/api/${API_VERSION}/openapi.jsonOpenAPI specification
/api/${API_VERSION}/openapi.yamlOpenAPI specification (YAML)

Every other route under /api/ needs a token.

Errors

401 from the token endpoint

Your username or password is wrong, or the account is disabled.

{
  "error": "invalid_grant",
  "error_description": "Invalid user credentials"
}

401 from the API

The Authorization header is missing, malformed, or the token has expired. Get a new one.

{
  "detail": "Not authenticated"
}

403 from the API

The token is valid but your account is not entitled to that data, for example the activity figures behind SAI and alerts.

{
  "detail": "No activity access"
}

Good practice

  • Keep the password out of your code. Read it from an environment variable or a prompt, not a committed file.
  • Reuse the token for its hour. Requesting a new one on every call is slow and needless.
  • Refresh rather than re-authenticate while the refresh token is still valid.
  • Never log a token. Anyone holding it has your access until it expires.
  • Always use HTTPS, so neither the password nor the token travels in the clear.