REST API

Authentication

Exchange an App Key for a token with POST /token, and learn what a token may do with GET /me.

POST /token#

Exchanges an App Key for a token. This is the only route that takes the key itself, and the only one without a token.

HTTP
POST /api/{domain}/token
Content-Type: application/json

{ "key": "<80-character App Key>" }
FieldMeaning
keyThe App Key.
ttl_secondsOptional. Shortens the token's life, 1 to 86400: it lives the shorter of this and the App Key's token TTL. It changes nothing else (below).
scopeOptional. Narrows what the token may do (Browser tokens).

Response 200:

JSON
{
	"data": {
		"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
		"token_type": "Bearer",
		"expires_in": 30
	}
}
FieldMeaning
tokenSend as Authorization: Bearer <token> or ?token=.
token_typeAlways Bearer.
expires_inSeconds until it expires: the App Key's token TTL, or shorter (ttl_seconds, a scope's).

Errors: 401 Invalid App Key (wrong format, unknown key, or a key of another Domain); 404 unknown Domain; 403 inactive Domain; 422 on ttl_seconds out of range, or an invalid scope. The key is checked first: a bad key is 401 whatever else is wrong.

curl -s -X POST https://api.thuum.dev/api/acme/token \
  -H 'content-type: application/json' \
  -d "{\"key\":\"$THUUM_APP_KEY\"}"
const response = await fetch('https://api.thuum.dev/api/acme/token', {
	method: 'POST',
	headers: { 'content-type': 'application/json' },
	body: JSON.stringify({ key: process.env.THUUM_APP_KEY })
});
if (!response.ok) throw new Error(`Token exchange failed: ${response.status}`);
const { data } = await response.json();
const { token, expires_in } = await thuum.exchangeToken('acme', process.env.THUUM_APP_KEY!);
import os, requests

response = requests.post(
    "https://api.thuum.dev/api/acme/token",
    json={"key": os.environ["THUUM_APP_KEY"]},
    timeout=10,
)
response.raise_for_status()
token = response.json()["data"]["token"]

Reusing tokens on a server#

A server making many REST calls can reuse a token until shortly before it expires:

JavaScript
let cached = null;

export async function thuumToken() {
	if (cached && cached.expiresAt - 5_000 > Date.now()) return cached.token;
	const response = await fetch(`${process.env.THUUM_API_URL}/api/acme/token`, {
		method: 'POST',
		headers: { 'content-type': 'application/json' },
		body: JSON.stringify({ key: process.env.THUUM_APP_KEY })
	});
	if (!response.ok) throw new Error(`Token exchange failed: ${response.status}`);
	const { data } = await response.json();
	cached = { token: data.token, expiresAt: Date.now() + data.expires_in * 1000 };
	return cached.token;
}

If a request still answers 401, drop the cached token, get a new one and retry once.

Shorter tokens#

ttl_seconds gives a token a shorter life than the App Key's TTL, with the same rights: unlike a scope, it narrows nothing. A token can only get shorter this way, never longer.

Use it for a token that only needs to live a moment, such as one handed to a browser just to open a WebSocket. The WebSocket checks the token when it connects; the connection then stays open after the token expires. To also limit what the browser may do with it, use a scope as well: the token then lives the shortest of the three.

curl -s -X POST https://api.thuum.dev/api/acme/token \
  -H 'content-type: application/json' \
  -d "{\"key\":\"$THUUM_APP_KEY\",\"ttl_seconds\":60}"
const { token } = await thuum.exchangeToken('acme', process.env.THUUM_APP_KEY!, {
	ttl_seconds: 60
});

What's in a token#

A token is a JWT signed by Thuum. Its payload holds the App Key's selector (sub), the issuer (iss, thuum.dev), and the issue and expiry times (iat, exp). It holds no permissions: those are looked up from the key's claims. You can read the payload for display (the client's decodeToken) but only Thuum can verify it.

GET /me#

Describes the caller: its Domain, App Key, token expiry and linked claims. Any valid token may call it; it needs no claim. Use it to show only the features a key may use, or to check a key's setup in a test.

Shell
curl -s https://api.thuum.dev/api/acme/me -H "authorization: Bearer $TOKEN"
JSON
{
	"data": {
		"domain": { "slug": "acme", "name": "Acme Inc.", "description": null },
		"key": {
			"selector": "3f9c1a7be04d52aa",
			"name": "web-browser",
			"token_ttl_seconds": 60,
			"inserted_at": "2026-10-20T09:12:44.000000Z"
		},
		"token": { "expires_at": "2026-10-21T14:03:12Z" },
		"domain_claim": null,
		"event_claim": {
			"id": "8b1d…",
			"name": "Browser chat",
			"description": null,
			"topic_flags": 0,
			"fallback_data_flags": 0,
			"topics": { "chat": 11 }
		},
		"state_claim": {
			"id": "c40e…",
			"name": "Browser presence",
			"description": null,
			"topic_flags": 0,
			"fallback_data_flags": 0,
			"topics": { "presence": 11 }
		}
	}
}

A claim is null when the key isn't linked to one of that kind. domain_claim lists its protocols as { "domain": 5, "event": 1 }.

TypeScript
import { resolveDataFlags, has, PUBLISH } from '@thuum/client';

const me = await acme.me();
const canPost =
	me.event_claim !== null && has(resolveDataFlags(me.event_claim, 'chat:lobby').flags, PUBLISH);