Guide
OAuth 2.1
For an app your customers install, OAuth is the way in: they paste a URL, click Authenticate, sign in to Kelomo and approve — no API key, no config file, nothing to copy. API keys remain the right tool for your own scripts and server-side jobs.
Which one do I want?
| OAuth 2.1 | API key | |
|---|---|---|
| Who is it for | Software other people install | Your own scripts and back-end jobs |
| How access is granted | The user approves a consent screen | Someone mints a key and copies it |
| Credential lifetime | 1 h access token, rotating refresh token | Until revoked or expired |
| Who can revoke it | The user, from their own settings | The key's owner or an admin |
Discovery
You only need one URL to start: the resource you want to reach. Everything else is discoverable, so nothing about Kelomo's endpoints has to be hard-coded into your client.
# 1. The resource tells you where its authorization server is.
curl https://mcp.kelomo.fi/.well-known/oauth-protected-resource
# → { "resource": "https://mcp.kelomo.fi/mcp",
# "authorization_servers": ["https://api.kelomo.fi"] }
# 2. The authorization server tells you everything else.
curl https://api.kelomo.fi/.well-known/oauth-authorization-server An unauthenticated request to the MCP endpoint answers with the same pointer, which is what lets a client start a login without being configured at all:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="kelomo", error="invalid_request",
error_description="Authorization required.",
resource_metadata="https://mcp.kelomo.fi/.well-known/oauth-protected-resource" Register a client
Registration is dynamic (RFC 7591) and anonymous — you do not need a Kelomo account to register, because a registration grants nothing on its own. Kelomo issues no client secret: every client is public, and PKCE is what proves the token request comes from the same software that started the authorization.
curl -X POST https://api.kelomo.fi/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Assistant",
"redirect_uris": ["http://127.0.0.1:1455/callback"]
}'
# → { "client_id": "kelomo_client_…",
# "token_endpoint_auth_method": "none",
# "grant_types": ["authorization_code", "refresh_token"] } redirect_uris are matched exactly. Accepted shapes are https,
a loopback http address (127.0.0.1, [::1],
localhost) and private-use schemes such as vscode://. For loopback
URIs the port may differ at authorization time — the operating system assigns it when your
app starts — but nothing else may.
Send the user to consent
Open a browser at the authorization endpoint. code_challenge_method must be
S256; plain is not accepted.
https://app.kelomo.fi/oauth/authorize
?response_type=code
&client_id=kelomo_client_…
&redirect_uri=http://127.0.0.1:1455/callback
&code_challenge=BASE64URL(SHA256(verifier))
&code_challenge_method=S256
&scope=worktime.supervise%20reports.team
&resource=https://mcp.kelomo.fi/mcp
&state=RANDOM
The user signs in, picks which organisation to connect and sees, in plain language, exactly
what your app will be able to do. Then they land back on your redirect_uri with
code and your state — or with
error=access_denied if they said no.
Exchange the code
curl -X POST https://api.kelomo.fi/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d client_id=kelomo_client_… \
-d code=THE_CODE \
-d code_verifier=THE_VERIFIER \
-d redirect_uri=http://127.0.0.1:1455/callback
# → { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600,
# "refresh_token": "…", "scope": "worktime.supervise" } The code is single-use and lives about a minute. Presenting it twice is treated as a leak rather than a retry: the connection is revoked and the user has to approve again. So exchange it once, and store what you get back.
Refresh
Refresh tokens rotate: every refresh returns a new one and spends the old. Always persist the new token before making the next call.
curl -X POST https://api.kelomo.fi/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=refresh_token \
-d client_id=kelomo_client_… \
-d refresh_token=THE_REFRESH_TOKEN
# → a new access token AND a new refresh token. The old one is now spent. Presenting a refresh token that has already been used means a copy of it exists somewhere it should not, so the whole connection is revoked. If your client can refresh from two processes at once, serialise it.
Scopes
A scope is a Kelomo capability — the same vocabulary that governs API keys and what a person
can do in the product. There is no separate OAuth permission language to learn, and no
.read/.write pairs.
Three things follow from that, and all three are worth designing for:
- Consent cannot manufacture authority. The granted scope is what you asked for intersected with what that member actually holds. An employee approving a payroll app grants nothing of payroll — and the consent screen tells them so before they approve.
- An empty scope is normal and useful. A connection with no scopes still
reaches everything under
/v1/me— the person's own hours, absences, trips and expenses. For most assistants that is the whole job. -
org.adminis never granted. It carries billing, settings and credential management, and an app that could mint an API key would outlive its own revocation. Integrations that genuinely administer an organisation use a service key, minted by a human.
Call GET /v1/me/access with an access token to see exactly which capabilities
the connection can exercise — the fastest way to explain a 403.
Revocation
Users disconnect apps from Settings → API access, and admins can end any connection in their organisation. Revocation is immediate: the next call with an existing access token fails, rather than working until the hour is up.
Your client can end its own connection with POST /v1/oauth/revoke. A password
change or reset also disconnects every app that account had authorised — expect to see a
401 and start the flow again.
Handle 401 by refreshing once; if the refresh also fails, the connection is
gone and the user needs to re-authorise.