Docs · The interface — every route

In STYGION Keystone

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.

Updated

The interface — every route

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

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:

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)

[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.

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

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:

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:

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

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:

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
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:

# 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.

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.

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.

Did this page help?

Opens the feedback panel with this page attached, and lands in the same queue as everything else.