# The interface — every route

> All 44 paths of the local API: the scope each needs, what it takes, what it answers — and how to wire your own tooling to it, from this machine and from outside.

Source: https://stygion.eu/docs/keystone/api-reference
Updated: 2026-09-07

This list is **not written from memory**. The mod generates a document about itself out of its own routes (`GET /v1/openapi.json`), so it cannot be wrong about what exists — and this page was pulled out of that with `scripts/reference.sh` in the mod's repository. When you are unsure, ask your server rather than this page.

Current for **0.1.1**. There are **44** paths.

---

## Before you call anything

### 1. Make a key

```
/keystone key new my-tool status.read players.read
```

A key is printed **once** and never again. Give it the narrowest set of scopes that does the job: a script watching whether the server is up wants `status.read` and nothing else.

### 2. Call it

```bash
curl -sS -H "Authorization: Bearer ks_…" \
  http://127.0.0.1:25586/v1/status
```

`GET` takes its parameters in the address; `POST` sends them as a form:

```bash
curl -sS -X POST -H "Authorization: Bearer ks_…" \
  -d "player=Steve" -d "group=vip" -d "for=30d" \
  -d "note=bought on the site" \
  http://127.0.0.1:25586/v1/players/group
```

### 3. What the answers mean

| Code | What it is |
|---|---|
| `200` | It worked. |
| `400` | Something in the request was wrong, and the answer says what. |
| `403` | The key is real but does not hold the scope this route needs. |
| `404` | No such route — **or no key at all**. Those answer identically on purpose, so an unauthenticated caller learns nothing about this server. |
| `429` | Past what this key is allowed per minute. |

---

## How to reach it

### From this machine only (the default, and the safe one)

```toml
[api]
enabled = true
address = "127.0.0.1"
port = 25586
```

Nothing is visible from outside. The panel and your scripts run on the same machine.

### From your own computer, over a tunnel (recommended on a hosted server)

Leave the mod on loopback and bring the port to you:

```bash
ssh -L 25586:127.0.0.1:25586 you@your-server
curl -sS -H "Authorization: Bearer ks_…" http://127.0.0.1:25586/v1/status
```

**Nothing is exposed**, and `/keystone panel` opens in your own browser.

### From your home network

```
/keystone set keystone.api.address 0.0.0.0
/keystone set keystone.api.reachable-at 192.168.1.100
```

The first says where it **listens**; the second says which address the mod writes into the editor link and into `openapi.json` — `0.0.0.0` is a good answer to the first and a useless one to the second. Then open the port to the LAN only, not the world:

```bash
sudo ufw allow from 192.168.1.0/24 to any port 25586 proto tcp
```

### From outside, behind a reverse proxy or a tunnel

Leave the mod on loopback and put something that speaks TLS in front of it:

```nginx
location /keystone/ {
    proxy_pass http://127.0.0.1:25586/;
    proxy_set_header Host $host;
}
```

```
/keystone set keystone.api.reachable-at keystone.yourserver.eu
```

**Never put `api.address` on a public address without TLS.** The key travels in a header, and over plain `http://` across the internet anybody on the path reads it. And the API can do everything the mod can do.

---

## The live stream

```bash
curl -N -H "Authorization: Bearer ks_…" \
  http://127.0.0.1:25586/v1/events
```

It holds the connection open and writes events down it as they happen (`text/event-stream`). Three lines in a browser:

```js
const events = new EventSource('/v1/events');
events.onmessage = (e) => console.log(JSON.parse(e.data));
```

It is authenticated like everything else and answers `404` without a key.

---

## Status and measurement

| Method | Path | Scope | What it is for |
|---|---|---|---|
| `GET` | `/v1/status` | `status.read` | What this server is and how it is doing |
| `GET` | `/v1/metrics` | `status.read` | What it is doing right now, and the last quarter of an hour |
| `GET` | `/v1/metrics/history` | `status.read` | A minute at a time, as far back as the settings keep it |
| `GET` | `/v1/entities` | `status.read` | What is loaded in the world, and what a sweep would remove right now |
| `GET` | `/v1/gate` | `status.read` | Whether the server is open, and how many places are held back |
| `GET` | `/v1/catalog` | `status.read` | Everything this pack has loaded: the registries, what is in one (`?of=minecraft:item`, with `from`/`limit`/`like`), or its tags (`&tags=true`) |
| `GET` | `/v1/bench` | `server.read` | Load test runs this server has made (`?id=` for one in full) |
| `GET` | `/v1/openapi.json` | `status.read` | This document |

```bash
curl -sS -H "Authorization: Bearer ks_…" \
  "http://127.0.0.1:25586/v1/catalog?of=minecraft:item&like=diamond&limit=20"
```

## People and permissions

| Method | Path | Scope | What it is for |
|---|---|---|---|
| `GET` | `/v1/players` | `players.read` | Who is online right now |
| `GET` | `/v1/progress` | `players.read` | What one player has done, or the board for one counter |
| `GET` | `/v1/skins` | `players.read` | What each player is wearing and where it came from |
| `GET` | `/v1/accounts` | `players.read` | How many accounts have a password, and who is waiting to prove themselves |
| `GET` | `/v1/bans` | `players.read` | Every ban that still stands |
| `GET` | `/v1/tickets` | `players.read` | The tickets on this server (`?id=` for one with everything said in it, `?player=`) |
| `GET` | `/v1/votes` | `players.read` | Votes this server has been sent, and how the party is doing (`?player=`, `?limit=`) |
| `GET` | `/v1/discord/links` | `players.read` | Which Minecraft accounts one Discord user has linked here (`?discord=<id>`) |
| `GET` | `/v1/groups` | `permissions.read` | Every permission group, heaviest first |
| `POST` | `/v1/players/check` | `permissions.read` | Whether somebody may do a thing, and which rule decided |
| `POST` | `/v1/players/group` | `permissions.write` | Put somebody in a group, or extend how long they are in it |
| `POST` | `/v1/players/perk` | `perks.write` | Give somebody a named benefit, optionally until a date |
| `POST` | `/v1/players/grants` | `perks.read` | What one player holds, where each came from and when it ends |

**What they take:**

| Path | Takes |
|---|---|
| `/v1/players/check` | `player` — the name of somebody online · `permission` — the permission to ask about |
| `/v1/players/grants` | `player` — the name of somebody online |
| `/v1/players/group` | `player` · `group` — the group id · `for` — `30d`, `12h`, `90m`, or leave it out for forever · `note` — why, for whoever reads this later |
| `/v1/players/perk` | `player` · `perk` — what they get · `for` · `note` |

### How to build a shop on it

On a server that is not in online mode, **a name typed into a form proves nothing**. The order is this:

```bash
# 1. Who that person is in game — by the Discord they signed in with
curl -sS -H "Authorization: Bearer ks_…" \
  "http://127.0.0.1:25586/v1/discord/links?discord=123456789012345678"

# 2. Deliver what they bought, with an end date
curl -sS -X POST -H "Authorization: Bearer ks_…" \
  -d "player=Steve" -d "group=vip" -d "for=30d" -d "note=order 4821" \
  http://127.0.0.1:25586/v1/players/group
```

The rank ends on the day by itself. Nobody has to remember to take it back.

## Operations

| Method | Path | Scope | What it is for |
|---|---|---|---|
| `GET` | `/v1/operations` | `operations.read` | What is scheduled and how long is left |
| `GET` | `/v1/operations/history` | `operations.read` | What has run lately, most recent first |
| `POST` | `/v1/operations/run` | `operations.run` | Run one now, or after a delay, with its usual warnings |
| `POST` | `/v1/operations/cancel` | `operations.run` | Call off what is pending and go back to the schedule |
| `POST` | `/v1/gate/maintenance` | `operations.run` | Close the server for maintenance, or open it again |
| `GET` | `/v1/backups` | `backup.read` | Every backup there is, newest first, and what they cost on disk |
| `POST` | `/v1/backups/restore` | `backup.restore` | Put a backup back — staged, and done at the start of the next boot |

**What they take:**

| Path | Takes |
|---|---|
| `/v1/operations/run` | `operation` — which one · `in` — how many seconds from now, `0` for immediately |
| `/v1/operations/cancel` | `operation` — which one |
| `/v1/gate/maintenance` | `closed` — `true` to close, `false` to open · `reason` — what is happening, shown to anybody who tries to join · `until` — when it is expected to end, in your own words |
| `/v1/backups/restore` | `backup` — the backup's name, or `cancel` to call one off |

## Settings and the record

| Method | Path | Scope | What it is for |
|---|---|---|---|
| `GET` | `/v1/settings` | `settings.read` | Every setting every module declared, with what each one is and what it holds |
| `POST` | `/v1/settings` | `settings.write` | Change one. Refused with the reason if the value does not fit what it is |
| `GET` | `/v1/settings/history` | `settings.read` | Every settings change, newest first, with what each value was before it |
| `POST` | `/v1/settings/rollback` | `settings.write` | Undo what one version changed. Undoing is itself a version, so it can be undone |
| `POST` | `/v1/reload` | `settings.write` | Re-read every settings file. Nothing restarts and nobody is disconnected |
| `GET` | `/v1/audit` | `audit.read` | Who changed what over this interface, newest first. Reading is not recorded; changes and refusals are |
| `GET` | `/v1/keys` | `keys.read` | The keys this server has issued, without their secrets |

**What they take:** `/v1/settings` takes `setting` (its full name, such as `operations.restart.when`) and `value`; `/v1/settings/rollback` takes `version`.

```bash
curl -sS -X POST -H "Authorization: Bearer ks_…" \
  -d "setting=operations.restart.when" -d "value=every 6h" \
  http://127.0.0.1:25586/v1/settings
```

Everything there is to set is on [every setting](/docs/keystone/settings-reference).

## Console, chat and Discord

| Method | Path | Scope | What it is for |
|---|---|---|---|
| `GET` | `/v1/console` | `console.read` | The last lines this server printed, oldest first. Held in memory, not in the database |
| `GET` | `/v1/chat` | `chat.read` | The last things said in chat, oldest first |
| `GET` | `/v1/events` | `events.read` | The live stream of events |
| `GET` | `/v1/discord` | `server.read` | Whether the Discord bridge is connected, and how far behind it is |

## The assistant

| Method | Path | Scope | What it is for |
|---|---|---|---|
| `POST` | `/v1/assistant/ask` | `assistant.write` | Ask it something. The answer comes back with this call |
| `POST` | `/v1/assistant/incident` | `assistant.write` | Hand it the last stretch of console and chat and ask what looks wrong |
| `POST` | `/v1/assistant/decide` | `assistant.write` | Agree to a proposed change, or leave it alone |
| `GET` | `/v1/assistant/proposals` | `assistant.read` | Changes it wants made and nobody has decided on yet |
| `GET` | `/v1/assistant/threads` | `assistant.read` | The conversations there have been, newest first |
| `GET` | `/v1/assistant/thread` | `assistant.read` | One conversation, oldest message first |

**What they take:** `/v1/assistant/ask` takes `question`; `/v1/assistant/decide` takes `proposal` (which one) and `answer` (`yes` or `no`).

---

## Addons on the JVM

If you are building an addon in Java you do not talk over HTTP. You compile against `keystone-api`, which travels inside that same jar (`META-INF/jarjar/eu.stygion.keystone.api-<version>.jar`) and has **no dependencies of its own**, not even a logging framework. It is how you register your own module, command, permission, source of ranks — and **your own routes**, which then appear in this list and in `openapi.json` exactly like ours.

---

## What is written down

Every change through the interface is an audit line: which key, what, when — and **every refusal too**. A key repeatedly trying what it may not do is the only sign you will get that an integration has been taken over. Sensitive commands are masked out of the audit, the console and the log.
