REST API Reference
The UrsaMU HTTP API runs on the same port as the WebSocket hub (default
4203). This document covers only the engine-built-in endpoints —
plugins (mail, jobs, bbs, help, wiki, builder, events) register their own
routes via registerPluginRoute() and document them in their own repos.
Verified against src/routes/ and src/app.ts for v2.6.0.
Contents
- Auth model
- Error responses
- WebSocket connection
- Auth router —
/api/v1/auth/* - Players & channels —
/api/v1/me,/api/v1/players/*,/api/v1/channels/* - DB Objects —
/api/v1/dbos,/api/v1/dbobj/:id - Scenes —
/api/v1/scenes/* - Config & text —
/api/v1/config,/api/v1/connect,/api/v1/welcome - UI manifest —
/api/v1/ui-manifest - Avatars —
/avatars/:id - Health —
/health - Plugin endpoints
Auth model
All protected endpoints require a JWT in the Authorization header:
Authorization: Bearer <token>
Obtain a token via POST /api/v1/auth or POST /api/v1/auth/register.
Tokens are signed with JWT_SECRET (set in .env). If JWT_SECRET is
unset in production the engine exits at boot. In dev a random per-process
secret is used and tokens are invalidated on every restart.
A global API rate limit applies per IP (apiRateLimits map in
src/app.ts). Auth endpoints have an additional per-IP brute-force
guard.
Error responses
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid body |
| 401 | Missing or invalid token |
| 403 | Valid token, insufficient permissions |
| 404 | Resource not found |
| 405 | Method not allowed |
| 429 | Rate limited (Retry-After header set) |
| 500 | Internal server error |
Error body shape:
{ "error": "Human-readable message" }
WebSocket connection
The hub shares the HTTP port. Two connection modes:
JWT pre-auth (web clients)
ws://localhost:4203?token=<jwt>&client=web
The player is authenticated immediately. On JWT reauth the engine
re-applies the connected flag and re-joins the player’s #cid and
#location rooms (v2.4.0 fix).
Classic connect (Telnet-style)
Connect without a token, then send connect <name> <password>.
Rate limiting
Each WebSocket is limited to 10 commands/sec. Excess commands are
silently dropped (warning logged server-side).
Auth router
Defined in src/routes/authRouter.ts. All endpoints accept POST only.
Path is matched by suffix on /api/v1/auth*.
POST /api/v1/auth
Login. Returns a JWT.
Body:
{ "username": "Alice", "password": "hunter2" }
Response 200:
{ "token": "<jwt>", "id": "5", "name": "Alice" }
Errors: 401 invalid credentials, 429 rate-limited.
POST /api/v1/auth/register
Create a new character. Returns a JWT.
Body:
{ "username": "Alice", "email": "alice@example.com", "password": "hunter2" }
Response 201:
{ "token": "<jwt>", "id": "5", "name": "Alice" }
POST /api/v1/auth/reset-password
Consume a one-time reset token and set a new password. Token comparison
is constant-time (v2.0.0 hardening). Expired tokens are cleaned up
opportunistically.
Body:
{ "token": "<one-time-token>", "newPassword": "newpw" }
Response 200:
{ "message": "Password updated successfully." }
Players & channels
Routed from src/app.ts; handlers in src/routes/playersRouter.ts.
GET /api/v1/me
Current player profile. Requires auth.
{
"id": "5",
"name": "Alice",
"flags": ["connected", "player"],
"data": { "description": "...", "avatar": "/avatars/5" }
}
GET /api/v1/players/online
List currently connected players. Requires auth.
[
{ "id": "5", "name": "Alice", "location": "The Lobby" },
{ "id": "8", "name": "Bob", "location": "The Library" }
]
GET /api/v1/channels
List all channels. No auth required.
[
{ "name": "Public", "header": "[Public]", "members": 4 }
]
GET /api/v1/channels/:name/history
Recent messages. Requires auth. Query param ?limit=N (default 20, max
500).
[
{ "sender": "Alice", "message": "Hi!", "timestamp": 1750536000000 }
]
DB Objects
src/routes/dbObjRouter.ts. All endpoints require auth.
GET /api/v1/dbos
List accessible objects. Optional ?flags=room filter.
GET /api/v1/dbobj/:id
Fetch a single object.
{
"id": "5",
"name": "Alice",
"location": "1",
"flags": ["connected", "player"],
"data": { "description": "..." },
"contents": []
}
PATCH /api/v1/dbobj/:id
Update object data, name, or description. Requires ownership or admin.
Body:
{ "data": { "description": "A tall figure in a green cloak." } }
The engine does not currently expose
PUT/DELETEfor/dbobj/:id
or any/attrssub-routes. Edit attributes via the&attrcommand or
a custom plugin route.
Scenes
src/routes/sceneRouter.ts. All endpoints require auth.
GET /api/v1/scenes
List active scenes the caller can see.
POST /api/v1/scenes
Create a scene.
Body:
{ "name": "The Heist", "type": "action", "roomId": "room-5" }
GET /api/v1/scenes/locations
List rooms that currently host scenes.
GET /api/v1/scenes/:id
Fetch a scene with its pose log and participants.
PATCH /api/v1/scenes/:id
Update scene metadata (owner or admin; ownerless scenes are adopted by
the patcher — v2.0.0 fix).
POST /api/v1/scenes/:id/pose
Add a pose, OOC line, or scene-set.
Body:
{ "msg": "Alice steps through the door.", "type": "pose" }
type: "pose" (default), "ooc", "set". When type is not "set",
msg is required (400 otherwise).
PATCH /api/v1/scenes/:id/pose/:poseId
Edit an existing pose. Owner or admin.
POST /api/v1/scenes/:id/join
Join a scene.
POST /api/v1/scenes/:id/invite
Invite a player. Owner only.
Body:
{ "playerId": "8" }
GET /api/v1/scenes/:id/export
Export as Markdown or JSON. Query: ?format=markdown (default) or
?format=json.
Config & text
src/routes/config.ts. No auth required.
GET /api/v1/config
Server config (name, version, ports, theme).
{
"game": { "name": "My Game", "version": "0.0.1" },
"server": { "http": 4203, "telnet": 4201 },
"theme": { "primary": "#...", "backgroundImage": "..." }
}
GET /api/v1/connect
Connect-screen text (Markdown). Reads
config.game.text.connect (default text/default_connect.txt) with a
path-traversal guard.
GET /api/v1/welcome
Post-login welcome text (from the texts DBO, id welcome).
GET /api/v1/404
Site 404 page content (from texts DBO, id 404). Falls back to a
default if no entry exists.
UI manifest
GET /api/v1/ui-manifest
Returns the list of UI components registered by plugins via
registerUIComponent(). Optionally authenticated — if a JWT is
presented, results are filtered by the caller’s privileges
(requires.flag, etc.).
[
{ "element": "myplugin-panel", "title": "My Panel", "url": "..." }
]
External script URLs are rejected at registration time (v1.9.3
hardening).
Avatars
GET /avatars/:id
Public avatar image. :id must match ^[a-zA-Z0-9_-]+$ — dots and
slashes are rejected. Looks for data/avatars/<id>.{png,jpg,gif,webp}.
Cached for 1 hour.
Health
GET /health (or GET /)
Liveness probe. No auth.
{ "status": "ok", "engine": "UrsaMU" }
Plugin endpoints
Plugins register routes via registerPluginRoute(prefix, handler). The
engine matches by startsWith(prefix) and forwards the request along
with the authenticated userId (or null).
Official plugins:
| Plugin | Base path | Repo |
|---|---|---|
| jobs | /api/v1/jobs |
UrsaMU/jobs-plugin |
| events | /api/v1/events |
UrsaMU/events-plugin |
| bbs | /api/v1/bbs |
UrsaMU/bbs-plugin |
/api/v1/mail |
UrsaMU/mail-plugin | |
| help | /api/v1/help |
UrsaMU/help-plugin |
| wiki | /api/v1/wiki |
UrsaMU/wiki-plugin |
| builder | /api/v1/building |
UrsaMU/builder-plugin |
Custom plugins register their own routes — see
Plugin Development.