The local API and keys
Everything the mod can do, for programs, behind keys that only carry what you gave them.
This page is older than Keystone 0.2.8, released 18 October 2026. Something may have changed since.
On this page 6 parts
The local API and keys
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.
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/settingstakescheck=trueto ask whether a value would be accepted without writing it. A list is answered asa, brather than[a, b], which could not be written back.POST /v1/players/ungroup(permissions.write, fieldsplayerandgroup) 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: 0is not an error./v1/players/groupand/ungroupfind 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/metricsis 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.
Did this page help?
Opens the feedback panel with this page attached. It lands in the same queue as everything else.