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.

An AI assistant is the one caller that needs no API key. An administrator approves it in the browser instead, and it is handed access of its own: agent connections below.

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.

Agent connections: an assistant an administrator approved

An API key is for a program you run. An AI assistant installed on somebody's computer, Codex or Claude Code, is connected another way, and no secret is created or copied for it. An administrator gives the assistant the address of the MCP endpoint, signs in to GRANSKA in the browser with email and password or with Google, and approves the connection on a page that names the assistant, the one organisation it reaches and what it allows. The commands are on the MCP page.

What such a connection is, next to a key:

  • It acts as the administrator who approved it, in that one organisation, with full read and write access to this API. There is no narrower level. Only an administrator of the organisation can approve one.
  • Its reach is this API; its tools are fewer. Over MCP the assistant is offered the eleven tools the MCP page lists. The approval covers every route here that the organisation is licensed for, and the same access token is accepted on them as a bearer token.
  • It owns the jobs it starts, and no others. A job is read and deleted by the credential that created it. A connection is a credential of its own, so its jobs are closed to an API key of the organisation, to another connection and to the web application, and theirs are closed to it.
  • Its tokens are short and it renews them itself. An access token lives 15 minutes and answers 401 TOKEN_EXPIRED after that, as a key's token does. The assistant holds a refresh token and renews without anybody signing in again, until the connection ends: 90 days after it was approved.
  • It can be disconnected, and that is immediate. Any administrator of the organisation disconnects it under Administer Organisation in the administration area, and its next request is refused. The same happens if the administrator who approved it stops being an administrator or loses the account. Deactivating an API key does not touch a connection, and disconnecting a connection does not touch a key.
  • What the API returns to it reaches the assistant's provider. The assistant is an external service, and its provider keeps and uses what it receives under its own terms. Our deletion of a job removes our copy and not theirs.

POST /v1/oauth/token is not part of this. It remains the client-credentials exchange for an API key and refuses every other grant_type, as above. An assistant finds the endpoints it uses from the MCP endpoint itself, which the MCP page describes.