API

Authentication

Get a token, send it with each request, revoke it, and create long-lived personal tokens.

Studio signs in with a cookie. Scripts, servers and other apps use a bearer token instead.

Get a token

POST /api/v1/auth/login

Send mode: "token" to get a token instead of a cookie.

curl -X POST 'http://127.0.0.1:8080/api/v1/auth/login' \
  -H 'Content-Type: application/json' \
  --data '{"email":"you@example.com","password":"YOUR_PASSWORD","mode":"token"}'
const res = await fetch('http://127.0.0.1:8080/api/v1/auth/login', {
	method: 'POST',
	headers: { 'Content-Type': 'application/json' },
	body: JSON.stringify({ email: 'you@example.com', password: 'YOUR_PASSWORD', mode: 'token' })
});
const { data } = await res.json();
const token = data.access_token;
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	body, _ := json.Marshal(map[string]string{
		"email": "you@example.com", "password": "YOUR_PASSWORD", "mode": "token",
	})
	res, err := http.Post("http://127.0.0.1:8080/api/v1/auth/login", "application/json", bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var out struct {
		Data struct {
			AccessToken string `json:"access_token"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
		panic(err)
	}
	fmt.Println(out.Data.AccessToken)
}
import requests

res = requests.post(
    "http://127.0.0.1:8080/api/v1/auth/login",
    json={"email": "you@example.com", "password": "YOUR_PASSWORD", "mode": "token"},
)
token = res.json()["data"]["access_token"]

Response:

{
	"data": {
		"access_token": "YOUR_ACCESS_TOKEN",
		"token_type": "Bearer",
		"expires_in": 28800,
		"user": {
			"id": "7685a20f71a2e3535cdee4566a8e1b9f",
			"email": "you@example.com",
			"display_name": ""
		}
	},
	"meta": { "request_id": "TFUQWRNZQKSA27FNQMFQPBVR72", "response_time": 271.8 }
}

The token lasts eight hours (expires_in is in seconds) and no cookie is set.

Send the token

GET /api/v1/auth/me

Add Authorization: Bearer <token> to every request.

curl 'http://127.0.0.1:8080/api/v1/auth/me' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
const res = await fetch('http://127.0.0.1:8080/api/v1/auth/me', {
	headers: { Authorization: `Bearer ${token}` }
});
console.log(await res.json());
req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/api/v1/auth/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
res = requests.get(
    "http://127.0.0.1:8080/api/v1/auth/me",
    headers={"Authorization": f"Bearer {token}"},
)
print(res.json())

Response:

{
	"data": {
		"id": "7685a20f71a2e3535cdee4566a8e1b9f",
		"email": "you@example.com",
		"display_name": ""
	},
	"meta": { "request_id": "2DOAELS4STJYJ5CVYUJXGC6RMI", "response_time": 2.3 }
}

Sign out

POST /api/v1/auth/logout

Logging out with a token revokes that token right away. The response is 204 with no body.

curl -X POST 'http://127.0.0.1:8080/api/v1/auth/logout' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
await fetch('http://127.0.0.1:8080/api/v1/auth/logout', {
	method: 'POST',
	headers: { Authorization: `Bearer ${token}` }
});
req, _ := http.NewRequest("POST", "http://127.0.0.1:8080/api/v1/auth/logout", nil)
req.Header.Set("Authorization", "Bearer "+token)
_, err := http.DefaultClient.Do(req)
requests.post(
    "http://127.0.0.1:8080/api/v1/auth/logout",
    headers={"Authorization": f"Bearer {token}"},
)

Personal API tokens

For a long-running server or a cron job, create a personal token in Account settings or with POST /api/v1/auth/tokens.

  • Up to 20 tokens per user, each expiring within a year.
  • The secret is shown once. Kanesh stores only a hash.
  • A token acts as its owner, but cannot manage tokens or change the password.

To accept a raw token in a custom header such as X-Api-Key, set AUTH_TOKEN_HEADER. Authorization keeps working.

Rules for credentials

  • Send exactly one credential. A cookie plus a token, or two headers, returns 401.
  • Tokens in the query string are never accepted.
  • A token alone does not allow calls from a browser on another origin. Set up CORS for that.
  • Sign-in is refused from another origin with 403 access_denied. Sign in from your server, not from a browser on a different site.

Troubleshooting

  • 401 authentication_required: check that you send one credential and that the token has not expired or been revoked.
  • 429 Too Many Requests: login has a stricter rate limit. Wait for the Retry-After time.

Next steps

Buy me a coffee