Developers

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.1API 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.

bash
# 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
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.

bash
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.

text
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

bash
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.

bash
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.admin is 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.