Starship Horizons
Log In
☰
Knowledge Base›Web API

Web API

The server answers plain HTTP requests under /api/, on the same port as the consoles. It is how the editors read content and save some of it, and it is how a tool of your own — a score board, a stream overlay, a test script — can look at a running game without opening a console.

This page covers the basics: where the API is, how requests and answers look, and how writing is authorized. The endpoints themselves are on three pages:

  • Web API Live State: the running session — status, mission, vessels, contacts, player ships, server load.
  • Web API Content: authored content — missions, vessel classes, components, factions, models, maps, roles, tasks, modules and the rest.
  • Web API Writes: creating, replacing and deleting content, and the rules that apply.

For the browser-side wrappers a widget or editor uses (CallAPI, WriteAPI and the rest) see Server Commands. For what runs inside the server, see Mission Scripting.

Where it is

http://<server>:1864/api/<name>

Port 1864 is the server's web port, the one players type into a browser. It is set by <webserver port="1864"> in the server's config.xml. When the server also runs its TLS port (1866 by default) the API is there too. There is nothing separate to open or forward: a machine that can load a console can reach the API.

Everything is JSON. Answers come back as application/json, and a write sends its record as a JSON body.

Paths

PathMeans
/api/{name}The whole collection, such as every faction.
/api/{name}/{id}One record by its key, such as one faction.
/api/{name}/{module}/{id}One record in a named module. Used to read encyclopedia entries and by the deletes that have to say which module's copy goes.

Endpoint names are not case sensitive. Ids are usually matched exactly, case included; a few kinds, such as roles, task definitions and bots, ignore case. Encode anything that is not a letter or a digit: the role Drone: Cargo is /api/archetypes/Drone%3A%20Cargo.

Reads and writes

Reads are open. A GET needs no token. It returns the data itself, with no envelope around it. A record that does not exist comes back as 200 with an empty body (a few endpoints answer {} instead), so test for an empty answer rather than for a status code. A name the server does not know is a 404 with an empty body.

Writes need the token. POST, PUT and DELETE must carry it, and they always answer with an envelope:

{ "ok": true,  "data": { ... } }
{ "ok": false, "error": "module-sealed" }

The error is a short code rather than a sentence, so a tool can branch on it. Web API Writes lists the codes and the status that goes with each.

The token

Every server has one API token. It lives in config.xml:

<server>
  ...
  <api-token>3f9c0d...</api-token>
</server>

If there is none when the server starts, it makes one, writes it to config.xml and notes it in debug.log. Send it as a bearer token:

Authorization: Bearer 3f9c0d...

The token is not scoped. Any request that carries it can make any write the API offers, so treat it like a password to your content.

On the server machine, a program can ask for it:

GET /api/auth/token        ->  { "token": "3f9c0d..." }

The server answers that only to a caller on the same machine — 127.0.0.1, ::1 or localhost. Anyone else gets 403. This is how Hydra Studio gets the token when you open it at http://localhost:1864/; the editor and admin pages also receive it from the server as they are served. See Modding With Hydra Studio.

From another machine, copy the token from config.xml into the tool that needs it.

To change it, stop the server and edit the value, or clear it to have a new one made at the next start. Anything that kept the old one, such as the editor's cached copy in the browser, has to be given the new one.

The endpoints behind Hydra Studio's assistant (POST /api/model/...) follow a slightly different rule: they accept a caller on the server machine without the token, and anyone else only with it.

Calling it from another site

The API sends no cross-origin headers. A web page served from somewhere other than the game server cannot read its answers from a browser. Pages the game server serves itself (consoles, widgets, the editors) can, and so can scripts and command-line tools, which are not bound by the browser's rules.

The catalog

The server describes its own read endpoints:

GET /api/endpoints
{
  "endpoints": [
    { "name": "status", "summary": "Current server state: mode, running mission, player.",
      "takes_id": false, "save_command": null },
    { "name": "components", "summary": "Component definitions: power, weapons, sensors, shields.",
      "takes_id": true, "save_command": "COMPONENT-SAVE" },
    ...
  ],
  "usage": "GET /api/{name} lists; GET /api/{name}/{id} returns one, where takes_id is true."
}

The catalog is built from the same table the server answers reads from, so it cannot fall out of date. It lists reads only: /api/auth/token, the writes and the assistant endpoints are not in it. save_command names the command the editors save that kind of record with (see Server Commands), and is null for kinds that have none.

A first look with curl

curl ships with Windows 10 and later; in PowerShell type curl.exe, because plain curl there means something else.

curl http://localhost:1864/api/status
{"State":"Playing","Mode":"Mission","Mission":"Fighter Escort","Players":"Horizons"}

curl http://localhost:1864/api/factions/Daichi
curl http://localhost:1864/api/archetypes/Pirate
curl http://localhost:1864/api/validate/Horizons

Watching a game from a script

The most common outside use is a program that polls. This one prints the player ships once a second while a mission is being played:

import json, time, urllib.request

BASE = "http://localhost:1864/api/"

def get(path):
    with urllib.request.urlopen(BASE + path) as reply:
        body = reply.read().decode("utf-8")
        return json.loads(body) if body.strip() else None

while True:
    status = get("status")
    if status and status["State"] == "Playing":
        for v in get("vessels") or []:
            if v.get("IsPlayer"):
                print(v["Name"], v["Integrity"], "/", v["MaxIntegrity"], v["Map"], v["Planet"])
    time.sleep(1)

Keep the rate sensible. Each call to /api/vessels builds the whole list, and once a second is plenty for a score board. Web API Live State describes the fields.

The API or packets?

The API answers when it is asked. Packets arrive the moment something changes. A display that has to keep pace with the game — a gauge, a radar, anything that moves — belongs in a widget that accepts packets; see Packets And Topics and Writing A Widget. The API is for reading content, for checks now and then, and for tools that live outside the game.

Last updated 19 October 2026