Files
horizon/docs/api/server-reference.md
Detimes d503874bff Let 26.2 clients join a Horizon server, off by default: handshake and packet ids
A server.toml option, [server] accept_older_clients (horizon-accept-older-clients), off
by default. On, a login handshake stating 776 opens an adapter::ClientSession on the
connection instead of being refused, and a 26.2 status ping is told its own protocol.
The session follows the connection's states as Session does, mirrored, and renumbers
packet ids both ways with the inverse of the generated 26.2 tables, in the blocking
login path and in the io_pool play path. Off, nothing changes.

Part of #10404.
2026-11-06 05:22:16 +00:00

16 KiB

Server reference

Everything horizon-server offers a host or a tool: its commands, every server.toml key and the control API. How to host: hosting.md; the design: server.md; stability and versions: stability.md.

Commands

horizon-server help prints:

horizon-server: host a Horizon (Minecraft Java Edition 26.3) server

Usage: horizon-server [--dir DIR] <command>

  init        Set up a server here: the EULA, the world, the port, who may join (asks, or takes flags:
              --eula --world NAME --seed SEED --port N --whitelist/--no-whitelist --player NAME... --op NAME
              --offline --yes)
  run         Run the server in the foreground; type console commands, Ctrl-C stops it cleanly. It
              restarts the game if it crashes.
  start       Run the server in the background, or resume a stopped supervisor (output goes to logs/supervisor.log)
  stop, restart
  status [--json]
              Up or not, TPS, MSPT, players, RAM and CPU
  cmd COMMAND...
              Run a console command on the running server and print what it answers
  report [SECONDS]
              Record a performance report for SECONDS (default 60): where the ticks' time goes,
              entities, chunks, network, memory; saved in reports/ (JSON and text, no player data)
  logs [-f] [-n N]
              The last N lines of the log (default 50); -f follows it
  console     Attach to the running server: its log as it happens, TPS, MSPT, players and RAM at the
              top, and a command line for console commands; Ctrl-D detaches
  mcp         An MCP server on stdin and stdout, for an AI agent to run and moderate the server
              (players, kick, ban, whitelist, logs, events, backups, settings, restart...)
  backup [NAME]
              Zip the world and the settings into backups/ (a running server saves first and stays up)
  backups [--json]
              List the backups
  restore BACKUP [--dimension ID] [--yes]
              Put a backup's world in place (the current one is kept in backups/); a running
              server restarts with it. --dimension (like minecraft:the_nether) brings back only
              that dimension and leaves the rest of the world as it is
  service install|uninstall|print [--system]
              Start the server at boot or login: a systemd unit on Linux (--system: for every user,
              with sudo), a launchd agent on macOS, a task at logon on Windows
  panel       A one-time login link for the web panel ([panel] in server.toml; 127.0.0.1:8130 by
              default)
  update [--check [--json]] [--version TAG] [--timeout SECONDS]
              Install the newest release (checked against GitHub's SHA-256), back up the world and
              restart on it; goes back by itself if the game isn't up within the timeout (300 s).
              --archive FILE --version NAME installs an archive you have
  rollback    Back to the release used before the update
  config [--json]
              The settings in server.toml
  config set KEY=VALUE... [--json]
              Change settings (for example world.difficulty=hard server.max_players=10), checked first;
              a running server takes the difficulty, default game mode and whitelist at once, the rest
              at its next start
  import FILE Write server.toml from a vanilla server.properties
  import-resources DIR
              Copy game files prepared elsewhere (by the launcher or horizon-prepare) into place, after
              checking they are complete and for this Minecraft version; the old copy stays on failure
  op NAME, deop NAME
  whitelist on|off|list, whitelist add|remove NAME
              Who may join (on the running server, or in the files when it is stopped)
  players [--json]
              Who is online, the operators, the whitelist and the bans
  kick NAME [REASON], ban NAME [REASON], pardon NAME, ban-ip ADDRESS|NAME [REASON], pardon-ip ADDRESS
              Moderate the running server, or edit ban files while it is stopped

  --dir DIR   The server folder (default: the current folder)

Settings live in server.toml; the server writes server.properties from it before every start.

server.toml

Horizon server settings (docs/server.md). The server writes them into server.properties before every start: edit this file, not that one. Unknown keys are refused. horizon-server init writes this file with these defaults; horizon-server config set SECTION.KEY=VALUE changes it.

[server]

Key Default In server.properties What it does
motd "A Minecraft Server" motd the line under the server's name in the server list
port 25565 server-port the port players connect to
bind "" server-ip listen on every address, or only on this one (server-ip)
max_players 20 max-players how many players can be online at once
online_mode true online-mode check players' Minecraft accounts (false: anyone joins under any name)
whitelist true white-list only whitelisted players and operators can join
enforce_whitelist false enforce-whitelist kick online players taken off the whitelist
view_distance 10 view-distance chunks sent around each player
simulation_distance 10 simulation-distance chunks around each player where mobs, crops and redstone tick
max_view_distance 0 horizon-max-view-distance past view_distance, saved chunks only, up to this (never generated; 0: off)
admin_feedback_to_ops true horizon-admin-feedback-to-ops operators see each other's commands ([name: ...], vanilla); false: only the log
accept_older_clients false horizon-accept-older-clients true: 26.2 clients join too, their packets translated (docs/hosting.md)

[world]

Key Default In server.properties What it does
name "world" level-name the world folder
seed "" level-seed empty: a random seed on the first start
game_mode "survival" gamemode survival, creative, adventure or spectator
difficulty "easy" difficulty peaceful, easy, normal or hard
hardcore false hardcore one life: a player who dies becomes a spectator

[properties]

Any other server.properties key, spelled as vanilla spells it, for example: spawn-protection = 16

[engine]

Key Default In server.properties What it does
path "" the game program; empty: the one installed beside horizon-server
resources "" the prepared game files; empty: resources/ here, else the usual places
restart_on_crash true start the game again when it crashes
low_memory false true: no transparent huge pages, ~60 MB less idle for ~1 % more tick time

[backups]

Key Default In server.properties What it does
every "6h" automatic backups while the server runs: 30m, 6h, 1d...; "0" turns them off
keep 12 automatic backups kept (backups made by hand are never deleted)

[updates]

Key Default In server.properties What it does
keep 1 other releases kept by updates, besides the one in use and the one rollback goes to

[panel]

Key Default In server.properties What it does
bind "127.0.0.1:8130" the web panel: this computer only, or a tailnet address (100.x); "" turns it off
allow_public false true lets bind be any address; never put the panel on the open internet

[api]

Key Default In server.properties What it does
bind "" the control API: "" a free port on 127.0.0.1; or a fixed one, like 127.0.0.1:8131 or a tailnet address
token "" its token (16 characters or more); "" a new random one at every start
allow_public false true lets bind be any address; the token is then all that guards the server

[transfer]

A proxy handing players over between servers (docs/server.md). Not vanilla's accepts-transfers (the transfer packet).

Key Default In server.properties What it does
enabled false true: take a proxy's player-* requests; needs Velocity forwarding (forwarding.secret)
expect_seconds 10 how long a player the proxy announced is waited for, when it names no time
max_expect_seconds 60 the longest the proxy may ask for
max_expected 64 players waited for at once
max_freeze_seconds 30 the longest a player stays frozen while it is handed over
max_hold_seconds 60 the longest a player arriving from another server is held before it is dropped

[discord]

Post the chat, joins, leaves and deaths to a Discord channel, drawn like the in-game chat (docs/hosting.md)

Key Default In server.properties What it does
webhook "" the channel's webhook URL (a secret: anyone with it can post there); "" off
events ["chat", "join", "leave", "death"] what is posted; "kick" too if you like
enabled true false pauses it and keeps the webhook

Control API (/api/v1)

HTTP and JSON on 127.0.0.1. A running server writes its address and a random token to .horizon/api.json, readable only by its own user; every request sends Authorization: Bearer <token> and a Host that is that address. Errors answer {"error": "..."} with 400, 401, 403, 404, 405 or 409. The web panel uses the same routes with its session.

Method Path Takes Answers What it does
GET /api/v1/status nothing phase, version, pid, uptime_seconds, engine_pid, engine_uptime_seconds, crashes, last_crash, sample (TPS, MSPT, players, RAM, CPU), update How the server does
GET /api/v1/metrics ?since=T (seconds since 1970) {"samples": [...]}: one a second, the last 10 minutes The samples taken after T, for graphs
GET /api/v1/logs ?since=N {"next": N, "lines": [...]} The log lines after the Nth (the last 2000 are kept)
GET /api/v1/events ?since=N (or Last-Event-ID), ?types=join,leave,kick,chat,chat_filtered,death,log,phase (all by default) with Accept: text/event-stream, server-sent events as they happen (id, event: the type, data: JSON); otherwise {"next": N, "events": [...]} Log lines, the supervisor's phase, and the game's joins, leaves, kicks, chat, messages the chat filter refused (chat_filtered, without their text) and deaths (the last 2000 kept)
POST /api/v1/command {"line": "<one command>"} the lines the command logged Run a console command on the running game
POST /api/v1/stop nothing {"ok": true} Stop the game and the supervisor
POST /api/v1/start nothing {"ok": true} Start the game while keeping the supervisor and API running
POST /api/v1/restart nothing {"ok": true} Restart the game
GET /api/v1/backups nothing {"backups": [{"name", "bytes"}]} List the backups
POST /api/v1/backups {"name": "..."} (optional) {"name": "<file>"} Back up now (a running game saves first and stays up)
DELETE /api/v1/backups ?name=<file> (the full name backups lists) {"name": "<file>"} Delete a backup
GET /api/v1/backups/download ?name=<file> the zip Download a backup
GET /api/v1/crash-reports nothing {"reports": [{"name", "bytes"}]}, newest first (the .txt files in crash-reports/) List the game's crash reports
GET /api/v1/crash-reports/read ?name=<file> (a name crash-reports lists) the report's text (text/plain) Read one crash report
POST /api/v1/restore {"name": "<backup>", "dimension": "minecraft:the_nether"} (dimension optional) {"name": "<backup>", "dimension": "<id>" or null} Restart the game with that backup's world, or only one dimension of it (what it replaces is kept in backups/)
POST /api/v1/update {"tag": "...", "timeout": seconds} (both optional) {"ok": true}; status says how it went (update) Restart on that installed release, or download the newest first; goes back if the game isn't up within the timeout
GET /api/v1/metrics/prometheus nothing the latest sample as Prometheus text: horizon_up, horizon_tps, horizon_mspt{stat}, horizon_players_online, horizon_memory_rss_bytes, horizon_loaded_chunks{dimension} and horizon_entities{dimension} (players and block entities apart)... Metrics for a monitoring tool to scrape
GET /api/v1/players nothing online (name, uuid, address, ping_ms, dimension, position, op_level), max_players, ops, whitelist (enabled, players), bans, ip_bans (with reason, source, created, expires) Who is online, and vanilla's lists
GET /api/v1/mods nothing engine (found, target, base, horizon, hooks) and native (id, version, sha256), from the native-mods.json the launcher wrote beside the game program The native mods the server's game program was built with
POST /api/v1/players/kick {"name", "reason"} (reason optional) {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Kick an online player
POST /api/v1/players/ban {"name", "reason"} (reason optional) {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Ban a player (kicked if online)
POST /api/v1/players/pardon {"name"} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Lift a player's ban
POST /api/v1/players/ban-ip {"ip", "reason"}, or {"name"} of an online player {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Ban an address (its players are kicked)
POST /api/v1/players/pardon-ip {"ip"} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Lift an address's ban
POST /api/v1/players/op {"name"} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Make a player an operator (op-permission-level)
POST /api/v1/players/deop {"name"} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Make a player no longer an operator
POST /api/v1/whitelist {"enabled": bool} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Turn the whitelist on or off
POST /api/v1/whitelist/add {"name"} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Whitelist a player
POST /api/v1/whitelist/remove {"name"} {"changed": bool, "output": [...]}: whether the list (or who is online) changed, and the console's lines Take a player off the whitelist (kicked if enforce-whitelist)
GET /api/v1/config nothing {"config": {...}}: server.toml's settings by section (api.token hidden) The settings
PATCH /api/v1/config {"changes": {"world.difficulty": "hard", ...}} saved, applied (taken by the running game at once), restart (from the next start), running Change settings as config set does; not engine.*, panel.*, api.*, nor RCON, query or management keys
POST /api/v1/panel/login nothing {"url": "..."} A one-time login link for the web panel (with the token only, not from the panel)