# The local API and keys

> Everything the mod can do, for programs, behind keys that only carry what you gave them.

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

Keystone answers HTTP on `127.0.0.1:25586`. The panel is one caller; your own
tooling can be another.

```
[api]
enabled = true
address = "127.0.0.1"
port = 25586
reachable-at = ""     # 0.1.1 and newer
```

**Loopback is the default and it is the safe one.** Anything else is reachable
by whoever can reach that machine, and the API can do everything the mod can do.
If you need it from another machine, put it behind an SSH tunnel rather than
binding it to the world.

`reachable-at` does not decide where it listens — it decides which address the
mod writes into the editor link and into `openapi.json`. In detail on [the panel
page](/docs/keystone/panel).

## Keys

```
/keystone key new <name> <scope> [scope ...]
/keystone key list
/keystone key revoke <id>
/keystone key own <id>
```

A key is printed **once**, when it is made, and never again — the same way a
person sees a password. Every key has a name, so the audit trail says who did
what, and a rate limit per minute. `/keystone key new` hands out nothing its maker
could not be handed.

`Authorization: Bearer <key>` on every call. No key answers `404` rather than
`403`: a route that says "forbidden" has told an unauthenticated caller that it
exists. A real key that lacks the scope the route needs gets `403` and a
sentence saying which scope is missing — that caller already knows the surface
is there.

**A key remembers whether it was issued to the owner.** `/keystone key list`
marks the owner's keys and says when each was made. A key from before owners were
recorded (say, the one a shop uses to write ranks) is not the owner's until the
owner says so with `/keystone key own <id>`; the log names each one at boot. A key
somebody named like one of the mod's own is never widened into it. The API reads
nobody before it knows them and at most a megabyte from anybody; MCP tool calls go
through the same rate limit, owner check and audit record as every other request.

## Scopes

A key carries the scopes it is given and nothing else. Asking for a scope this
server has nothing behind is refused, and the refusal lists what does exist —
so a typo is caught when the key is made rather than when it is used.

| Scope | Reaches |
|---|---|
| `status.read` | Whether the server is up, who is on, how it is doing. |
| `players.read` | The people — including which game accounts one Discord user has linked. |
| `permissions.read` · `permissions.write` | Groups and what they may do. |
| `operations.read` · `operations.run` | The timetable, and running one by hand. |
| `settings.read` · `settings.write` | Every setting. |
| `console.read` | The server's log. |
| `chat.read` | What is being said. |
| `events.read` | The live stream (below). |
| `audit.read` | Who changed what. |
| `keys.read` | Which keys exist — never their secrets. |
| `backup.read` · `backup.restore` | Backups, and putting one back. |
| `perks.read` · `perks.write` | What people have earned. |
| `assistant.read` · `assistant.write` | The in-game assistant. |
| `languages.write` | The translations players read. |
| `server.read` | The machine underneath. |

**Some scopes are the owner's:** `settings.write`, `permissions.write`,
`perks.write`, `backup.restore`, `operations.run`, `assistant.write` and
`languages.write`. The router checks them on every request — whatever wildcard a
key holds, and over MCP too — and they pass only with a key issued to the console
or a level-4 operator. That is why a panel link reached through a group is read
and moderate only.

**Give a key the narrowest set that does the job.** A monitoring script wants
`status.read` and nothing else.

## What is there

`GET /v1/openapi.json` lists every path this server actually has — the mod
writes that document out of its own routes, so it cannot be wrong about what
exists. When you want the list, ask the server rather than this page.

Roughly: `/v1/status`, `/v1/players` (plus `/v1/players/group`,
`/v1/players/ungroup`, `/v1/players/perk`, `/v1/players/check`,
`/v1/players/grants`), `/v1/groups`, `/v1/operations` (`/run`, `/cancel`,
`/history`), `/v1/backups` (`/restore`), `/v1/settings` (`/history`, `/rollback`),
`/v1/console`, `/v1/chat`, `/v1/events`, `/v1/audit`, `/v1/keys`, `/v1/bans`,
`/v1/tickets`, `/v1/votes`, `/v1/skins`, `/v1/progress`, `/v1/metrics`
(`/history`), `/v1/bench`, `/v1/catalog`, `/v1/entities`, `/v1/gate`
(`/maintenance`), `/v1/accounts`, `/v1/discord` (`/links`), `/v1/assistant/*` and
`/v1/reload`.

- `POST /v1/settings` takes `check=true` to ask whether a value would be accepted
  without writing it. A list is answered as `a, b` rather than `[a, b]`, which
  could not be written back.
- `POST /v1/players/ungroup` (`permissions.write`, fields `player` and `group`)
  removes only the group grants the calling key made — what an admin typed, what
  Discord granted and what another key gave stay — and answers
  `{player, group, removed}`; `removed: 0` is not an error. `/v1/players/group` and
  `/ungroup` find a player who is not online but has joined this server before,
  because ranks are bought and run out while people are away. A name this server
  has never seen, or a group that does not exist, is a 400.
- The tick time in `/v1/metrics` is what a tick costs now, from the server's own
  tick times — not 1000 ÷ TPS.

## The live stream

`GET /v1/events` with `events.read` holds the connection open and writes server
events down it as they happen — which is what makes the panel a panel rather
than a page you refresh. It is authenticated like everything else, and answers
`404` without a key.

## 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/`) and has **no dependencies of its own**, not even a logging
framework. It is how you register your own module, command, permission or source
of ranks.

## What is written down

Every change through the API is an audit line: who, what, when. Sensitive
commands are masked out of it, the console and the log — Keystone's own
automatically, and anything you name in `keystone.mask-commands`.
