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.
https://api.granska.cloud/v1/oauth/tokenExchanges a client id and secret for a short-lived bearer token.
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.
| Parameter | Description |
|---|---|
grant_type"client_credentials"·body·required | The only grant this gateway supports. |
client_idstring·body·required | The identifier issued with the API key. |
client_secretstring·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.
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"
}'{
"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.
| Error | When |
|---|---|
400BAD_REQUEST | client_id or client_secret is missing. |
400UNSUPPORTED_GRANT_TYPE | grant_type is anything other than client_credentials. |
401UNAUTHORIZED | The client id is unknown, or the secret does not match it. |
500INTERNAL_ERROR | An unexpected server-side failure. Not probable from outside — reaching it means something is wrong. |
503SERVICE_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_EXPIREDafter 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.