GRANSKA

Authentication

Every endpoint except GET /v1/health and the token endpoint itself requires a bearer token. Tokens are obtained with the OAuth 2.0 client-credentials grant: there is no user in this flow and no redirect — a server proves it is a tenant, and gets a token scoped to that tenant.

POSThttps://api.granska.cloud/v1/oauth/token

Exchanges a client id and secret for a short-lived bearer token.

No credentialsSpends no quota

Where the credentials come from

An organisation administrator creates an API key in the web application, under API-nycklar in the administration area. Creating one produces a pair:

  • client_id — an identifier. It is listed beside the key from then on and is not a secret.
  • client_secret — shown once, at the moment of creation, and never again. Only a salted scrypt hash of it is stored, so a lost secret cannot be recovered; the key is revoked and a new one created in its place.

A key can be deactivated from the same screen. A deactivated key stops issuing tokens immediately, but a token already minted from it stays valid until it expires — deactivation is not revocation of tokens in flight.

Keys belong to one tenant. The token carries that tenant, and every call made with it reads and writes only that tenant's data. No parameter changes which tenant a call acts on.

Requesting a token

POST /v1/oauth/token with a JSON body. grant_type must be client_credentials; anything else is rejected rather than ignored.

ParameterDescription
grant_type
"client_credentials"·body·required
The only grant this gateway supports.
client_id
string·body·required
The identifier issued with the API key.
client_secret
string·body·required
The secret issued with the API key. Sent only to this endpoint, never on other calls.

The response carries access_token, token_type and expires_in. Send the token on every subsequent call as Authorization: Bearer <access_token>.

The client_secret is sent to this endpoint and to nothing else. If you find yourself putting it on another call, something is wrong.

Request
curl -X POST https://api.granska.cloud/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "cli_9f3a2b7c",
    "client_secret": "sk_live_2c8e41d0a7b6"
  }'
Response
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Token lifetime

A token lives one hour. expires_in says so in seconds, and it is the value to trust rather than a constant copied into your client.

There is no refresh token. When a token expires, request another one the same way. Requesting a token spends no quota, so a client that fetches one per hour — or simply on every 401 — is behaving correctly. Caching one across process restarts is an optimisation, not a requirement.

An expired token answers 401 TOKEN_EXPIRED, a distinct code from 401 UNAUTHORIZED precisely so a client can tell "get a new token and retry" apart from "these credentials are wrong, and retrying will not help".

Failures worth handling separately

401 UNAUTHORIZED on the token call means the client id is unknown, the secret does not match it, or the key has been deactivated. The response does not distinguish the three, deliberately: a caller probing for valid client ids learns nothing from the status.

503 SERVICE_UNAVAILABLE is retryable, and it is the one failure here that is. It means the credential store was transiently unreachable, not that the credentials are bad. The response carries a Retry-After header; back off for that many seconds and try again. Treating it as a permanent authentication failure — clearing stored credentials, paging someone — is the wrong reaction to a condition that clears itself.

500 INTERNAL_ERROR is worth one retry and no more. At any 4xx the request itself is the problem and the answer will not change.

ErrorWhen
400
BAD_REQUEST
client_id or client_secret is missing.
400
UNSUPPORTED_GRANT_TYPE
grant_type is anything other than client_credentials.
401
UNAUTHORIZED
The client id is unknown, or the secret does not match it.
500
INTERNAL_ERROR
An unexpected server-side failure. Not probable from outside — reaching it means something is wrong.
503
SERVICE_UNAVAILABLE
The credential store is transiently unavailable; a Retry-After header says when to try again. Not probed: it needs an injected infrastructure failure.