-
Notifications
You must be signed in to change notification settings - Fork 0
API
This page is for client and tool developers. Everything here is drawn from the bridge
implementation; where the umbrella API spec and the code disagree, the code is
authoritative. The current API revision is 1 and the bridge version is
0.0.1-alpha.1.
Every authenticated response is scoped to the token's player — a paired device sees and
acts on its own player's data only. Game state is only ever touched on the server
thread; I/O threads marshal work across via server.execute(...).
All routes live under a versioned prefix. There are two ways to reach them:
-
Direct / LAN:
http://<host>:25580/api/v1/...and WebSocketws://<host>:25580/ws/v1. The port isportfrom the config (25580by default). -
Relay:
https://<relay>/s/<serverId>/api/v1/...and WebSocketwss://<relay>/s/<serverId>/ws/v1. The relay forwards frames verbatim, so the API is byte-for-byte identical; only the base URL differs. Here<serverId>is the relay's short Server ID (case-insensitive) from/nerolink setup. See Relay.
Relay transport note. Over the relay, REST calls are multiplexed on one tunnel with request ids and WebSocket frames pass straight through (wire protocol in the
nerolink-relayrepo'ssrc/protocol.ts). Two error statuses are produced by the relay itself, not the bridge:503 BRIDGE_OFFLINEwhen the tunnel is down, and504 BRIDGE_TIMEOUTif the bridge doesn't answer within 30 s.
Every REST response is JSON with a top-level ok flag:
{ "ok": true, "data": { ... } }{ "ok": false, "error": { "code": "RATE_LIMITED", "message": "rate limit exceeded", "retryAfterMs": 1200 } }retryAfterMs is present only on 429 responses. The HTTP status code carries the same
signal as the code (see Errors).
-
Public (no token):
POST /api/v1/pairandGET /api/v1/privacy/notice. -
Everything else requires
Authorization: Bearer <token>and is charged against that token's rate-limit bucket.
You obtain a token by pairing:
- A player runs
/nerolink pairin-game and reads their one-time code (XXXX-XXXX, 5-minute TTL, single-use). - The client posts it:
On success:
POST /api/v1/pair { "code": "AB12-CD34", "deviceName": "Pixel 8" }The plaintext{ "ok": true, "data": { "token": "<long-lived bearer token>", "playerUuid": "…", "playerName": "Steve", "serverId": "…", "serverName": "Neroland SMP" } }tokenis returned once and never stored server-side (only a SHA-256 hash is kept). Send it as the Bearer token on every subsequent call. An invalid or expired code returns401 UNAUTHORIZED; exceeding the device cap returns429 RATE_LIMITED("device limit reached").
Tokens expire after tokenExpiryDays of inactivity (checked lazily). A client can revoke
its own token with DELETE /api/v1/session.
All paths are relative to /api/v1. A = requires auth.
| Method | Path | A | Purpose |
|---|---|---|---|
POST |
/pair |
— | Redeem a pairing code for a device token. |
GET |
/privacy/notice |
— | The server's data-processing notice. |
DELETE |
/session |
✓ | Revoke the calling device's token. |
GET |
/discovery |
✓ | API revision, versions, server identity, capability map. |
GET |
/privacy/export |
✓ | Everything the bridge holds for you (device metadata + prefs). |
POST |
/privacy/erase |
✓ | Fire Core's shared erasure for your data across all mods. |
GET |
/prefs/notifications |
✓ | Your notification category flags. |
PUT |
/prefs/notifications |
✓ | Replace your notification category flags. |
GET |
/{module}/{section} |
✓ | A module snapshot (player-scoped). |
POST |
/actions/{module}/{action} |
✓ | Invoke a safe, server-validated action. |
An unknown authed route returns 404 NOT_FOUND; an unsupported method on
/prefs/notifications returns 405 VALIDATION.
GET /api/v1/discovery is how a client builds its UI from exactly what the server
supports:
{ "ok": true, "data": {
"apiRevision": 1,
"bridgeVersion": "0.0.1-alpha.1",
"coreVersion": "2.0.0",
"server": { "id": "1a2b3c", "name": "Neroland SMP", "online": true, "players": 3 },
"modules": [
{ "id": "core", "version": "2.0.0", "schema": 1,
"data": ["gates", "alerts", "energy", "storage", "mods"], "actions": ["ack_alert"] },
{ "id": "nerospace", "version": null, "schema": 0, "data": [], "actions": [], "absent": true }
]
} }Every module the app knows about is emitted: present modules carry their version,
schema, data sections and actions; modules that aren't installed are emitted with
"absent": true (and version: null, schema: 0). The server.id here is a stable
per-world hash of the world name — distinct from the relay Server ID used in the base URL.
The bridge itself provides the built-in core module (schema 1), so a Core-only
server is fully functional. Snapshots come from GET /api/v1/core/{section}.
The four Core progression gates with per-player unlocked state (falls back to server-scope openness when the player is offline):
{ "asOf": 1751880000000, "gates": [
{ "id": "industrial_power", "unlocked": true },
{ "id": "reached_orbit", "unlocked": false },
{ "id": "first_colony", "unlocked": false },
{ "id": "deep_space", "unlocked": false }
] }The player's own active alerts, from Core's alert service:
{ "asOf": 1751880000000, "alerts": [
{ "id": "…", "module": "nerologistics", "severity": "WARNING",
"text": "Drone bay offline", "at": 1751879000000, "acked": false, "snoozed": false }
] }Well-formed but empty in v1, each with a note. Core exposes no cheap per-player
index of energy/storage blocks, and the bridge never scans loaded chunks. The schema is
additive, so a future Core index lights these up without a client change:
{ "asOf": 1751880000000, "energy": [],
"note": "Per-player energy index not available in Core v2; chunk scanning is disallowed. …" }A server-wide snapshot (not player-scoped — a mods list is public metadata) of the installed Neroland mods plus the running loader and MC version, so a client can render a mods overview and drive update checks:
{ "ok": true, "data": {
"asOf": 1751880000000,
"loader": "neoforge",
"mcVersion": "26.2",
"mods": [
{ "id": "nerolandcore", "name": "Neroland Core", "version": "2.0.0" },
{ "id": "nerolink", "name": "NeroLink", "version": "0.0.1-alpha.1" },
{ "id": "nerologistics", "name": "NeroLogistics", "version": "0.0.1-alpha.1" }
]
} }The list is collected once per loader at init and sorted by id for stable ordering.
POST /api/v1/actions/{module}/{action} invokes a safe action. The bridge re-validates
server-side before running anything:
-
Config gates first — if
readOnlyis on, or themodule/actionid is inactionsDisabled, the request is refused with403 ACTION_DISABLED. -
Module/action presence — unknown module or action →
404 MODULE_ABSENT. -
Idempotency — if the body carries a
requestId, a repeated call replays the cached response (dedup window is per player). -
Offline gating — unless the action declares
allowOffline(andallowOfflineActionsisn't forcing online-only), an offline player gets409 PLAYER_OFFLINE_REQUIRED. - The owning mod executes on the server thread and returns a result mapped to the envelope.
Acknowledge — and optionally snooze — one of your own alerts:
POST /api/v1/actions/core/ack_alert
{ "alertId": "…", "snoozeMs": 3600000, "requestId": "optional-idempotency-key" }
-
alertIdis required; omitting it returns400 VALIDATION. - With
snoozeMs, the alert is snoozed until now +snoozeMs; without it, the alert is acknowledged. - If the alert isn't one of the player's, the result is
403 NOT_OWNER. -
ack_alertisallowOffline— it works while the player is offline.
Success:
{ "ok": true, "data": { "alertId": "…", "acked": true } }Other mods register their own actions via Core's link registry; the framework (config gates, ownership/gate re-validation, idempotency, offline handling, error mapping) is the same for all of them.
Connect to GET /ws/v1 (direct) or .../s/<serverId>/ws/v1 (relay). The upgrade is
Bearer-authenticated: send Authorization: Bearer <token> on the upgrade request — an
invalid token is rejected with 401 UNAUTHORIZED before the handshake. One socket is kept
per device (a new socket for the same token replaces the old one).
Client → server control frames (JSON text):
{ "op": "sub", "topics": ["core.alerts", "core.energy", "nerologistics.drones"] }
{ "op": "unsub", "topics": ["core.energy"] }
{ "op": "ping" }A topic is moduleId.section — the same sections as the snapshot endpoints.
Server → client frames:
- On subscribe, an immediate consistent-start snapshot for each newly-subscribed
topic:
{ "topic": "core.alerts", "t": 1751880000000, "snapshot": true, "data": { … } } -
Deltas, coalesced per
(connection, topic)and flushed at most once per second:{ "topic": "core.alerts", "t": 1751880000000, "delta": [ { … }, { … } ] } - A reply to your
ping:{ "op": "pong" }, and a server heartbeat every 30 s:{ "op": "ping", "t": 1751880000000 }. Standard WebSocket ping/pong control frames are also honoured.
Player-scoped events are delivered only to that player's sockets; broadcast events go to
everyone. Unknown op values are ignored for forward-compatibility.
-
GET /api/v1/privacy/notice(public) —{ "notice": "…" }, the configuredprivacyNoticeText. -
GET /api/v1/privacy/export— device metadata (deviceId,deviceName,createdAt,lastSeenAt,thisDevice) and your notification prefs. Token hashes are never included. -
POST /api/v1/privacy/erase— fires Core's sharedPlayerDataErasure(fanning out across every mod), purges the bridge's own token/prefs/pending-code state, and drops your live sockets. Returns{ "erased": true, "scope": "bridge" }. -
GET/PUT /api/v1/prefs/notifications— read/replace your per-category notification flags ({ "notifications": { "nerologistics": true, … } }). Categories are opt-in (default off). See Privacy.
Shared error codes and their HTTP statuses:
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED |
401 | Missing/invalid bearer token, or bad pairing code. |
TOKEN_REVOKED |
401 | Token no longer valid (revoked). |
RATE_LIMITED |
429 | Rate limit or device cap hit; carries retryAfterMs. |
VALIDATION |
400 (405 on bad method) | Malformed request / missing field. |
NOT_OWNER |
403 | The target isn't the caller's own data. |
GATE_LOCKED |
403 | A required progression gate isn't unlocked. |
ACTION_DISABLED |
403 |
readOnly on, or the action is in actionsDisabled. |
PLAYER_OFFLINE_REQUIRED |
409 | The action needs the player online. |
MODULE_ABSENT |
404 | Module or action not present. |
NOT_FOUND |
404 | No such route/path. |
INTERNAL |
500 | Unexpected server error. |
Relay-only: BRIDGE_OFFLINE (503) and BRIDGE_TIMEOUT (504), described above.
-
Per-token REST budget:
rateLimitPerMinute(default60) requests per rolling minute, token-bucket. On breach:429 RATE_LIMITEDwithretryAfterMs. -
Client cap:
maxClients(default64) — enforced at pairing as a per-player device limit; a new pairing past the cap returns429 RATE_LIMITED("device limit reached").