// developer reference

PulseConnect API Reference

REST + WebSocket reference for the PulseConnect plugin. Build a Discord bot, a web dashboard, a CLI, or your own custom client on top of any PulseConnect-enabled server.

v1 · HTTP + WebSocket · JWT auth
Download .md Resources
heads up

This is the developer reference. It documents the full PulseConnect API surface, including multi-user staff accounts and granular permissions. The PulsePanel iOS app uses owner-only mode and does not expose those features in its UI. If you're just running the app, you don't need this document — see the setup guide instead.

For developers building custom integrations. If you want to use a Discord bot, web dashboard, CLI, or your own custom client to talk to a PulseConnect-enabled server, this is the document.

The PulsePanel iOS app is the official client and the easiest way to use PulseConnect, but the API is fully documented and not coupled to the app. Everything below is fair game for third-party integrations.

Conventions

Base URL

http://<your-server>:7070/api/v1

Default port is 7070, configurable via api.port in config.yml. The plugin serves plain HTTP — if you're exposing the API to the internet, put it behind a reverse proxy (nginx, Caddy, Cloudflare) for HTTPS.

Authentication

All endpoints except POST /auth/login and POST /auth/register require a JWT in the Authorization header:

Authorization: Bearer <token>

Tokens are obtained via login (see below). They expire after 24 hours by default (api.token-expiry-hours in config). When a token expires, the server returns 401 Unauthorized and the client should re-login.

Content type

All request bodies are JSON. All response bodies are JSON. Set Content-Type: application/json on requests.

Errors

Failed requests return an HTTP error status (4xx or 5xx) with a JSON body:

{ "error": "Human-readable message" }

Common status codes:

StatusMeaning
400Bad request (missing/malformed body)
401Not authenticated (missing or invalid token)
403Authenticated but lacks the required permission
404Resource not found
409Conflict (e.g., duplicate username, file edit conflict)
429Too many requests (login rate limiting)
500Server error

Permissions

Most endpoints require a specific permission. The Owner role bypasses all permission checks. Other staff have the permissions explicitly granted to them by the Owner.

The full permission catalog is documented at the bottom of this page.

Auth

MethodPathPermissionDescription
POST/auth/loginnoneExchange username/password for a JWT
POST/auth/registerbootstrap or OwnerCreate the first user (bootstrap) or another user
GET/auth/meany auth'dGet the caller's username, role, permissions
POST/auth/me/passwordany auth'dChange own password
GET/auth/usersOwnerList all staff
POST/auth/usersOwnerCreate a staff user
DELETE/auth/users/{username}OwnerDelete a staff user
GET/auth/users/{username}/permissionsOwnerGet a user's permissions
PUT/auth/users/{username}/permissionsOwnerReplace a user's permission set
POST/auth/users/{username}/passwordOwnerReset another user's password

Bootstrap and registration

POST /auth/register creates user accounts. There are two distinct modes:

Bootstrap mode — when zero accounts exist in the database (fresh install, or after the database has been wiped), /auth/register is callable without authentication. The first call creates the Owner. Once any account exists, bootstrap mode closes permanently and the endpoint requires an Owner JWT.

In practice, the Owner is normally created automatically from the admin: block in config.yml on first server startup, and direct calls to /auth/register are only needed for the post-bootstrap case (creating additional staff users, which require Owner auth).

Request body for both modes:

{
  "username": "new-user",
  "password": "their-password",
  "role": "staff"
}

role is "owner" or "staff". Only one Owner exists per server; attempting to create a second returns 409 Conflict.

Login (worked example)

POST /api/v1/auth/login
Content-Type: application/json

{
  "username": "admin",
  "password": "your-password"
}

Response:

{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "username": "admin",
  "role": "owner"
}

After 5 failed attempts in 15 minutes (configurable), the (username, IP) pair is locked out and 429 Too Many Requests is returned. The lockout window resets on a successful login.

/auth/me response shape

{
  "username": "admin",
  "role": "owner",
  "permissions": ["dashboard.view", "console.view", "..."]
}

For Owner accounts, permissions is the full catalog. For other roles, it's only what the Owner has granted.

Server

MethodPathPermissionDescription
GET/server/statusdashboard.viewTPS, memory, uptime, player counts
POST/server/reloadserver.reloadReload the server config
POST/server/restartserver.restartRestart the server
POST/server/stopserver.stopStop the server
GET/server/console-historyconsole.viewLast 100 lines of server console

Status (worked example)

GET /api/v1/server/status
Authorization: Bearer <token>

Response:

{
  "online": true,
  "version": "Paper 1.21.4",
  "motd": "A Minecraft Server",
  "onlinePlayers": 3,
  "maxPlayers": 20,
  "tps1m": 19.98,
  "tps5m": 19.95,
  "tps15m": 19.93,
  "usedMemoryMb": 2048,
  "maxMemoryMb": 8192,
  "uptimeMs": 14400000
}

Players

MethodPathPermissionDescription
GET/playersplayers.viewList online players
GET/player/{uuid}players.viewGet one player's details
POST/player/{uuid}/healthplayers.edit_statsSet health (0–20)
POST/player/{uuid}/hungerplayers.edit_statsSet hunger (0–20)
POST/player/{uuid}/gamemodeplayers.edit_statsSet game mode
POST/player/{uuid}/opplayers.opGrant or revoke OP
GET/player/{uuid}/inventoryplayers.inventoryInventory + armor + ender chest
POST/player/{uuid}/inventory/giveplayers.inventoryGive an item
POST/player/{uuid}/inventory/setplayers.inventorySet a specific slot
DELETE/player/{uuid}/inventoryplayers.inventoryClear inventory

Moderation

MethodPathPermissionDescription
POST/moderation/kickplayers.kickKick a player with a reason
POST/moderation/banplayers.banBan (permanent or timed)
DELETE/moderation/ban/{uuid}players.banUnban
GET/moderation/bansplayers.banList active bans
GET/moderation/bans/{uuid}/historyplayers.banBan history for one player
POST/moderation/muteplayers.muteMute a player
DELETE/moderation/mute/{uuid}players.muteUnmute
GET/moderation/mutesplayers.muteList active mutes
POST/moderation/flagplayers.flagFlag a player for staff attention
DELETE/moderation/flag/{uuid}players.flagUnflag
GET/moderation/flaggedplayers.flagList flagged players

Console / Commands

MethodPathPermissionDescription
POST/commandconsole.executeExecute a console command

Body: { "command": "say Hello world" }

Response: { "success": true, "message": "..." }

stop, restart, and reload are blocked at the command level — use the dedicated /server/* endpoints instead.

Tickets

MethodPathPermissionDescription
GET/tickets?status={open|closed}tickets.viewList tickets, optionally filtered
GET/tickets/{id}tickets.viewTicket detail with messages
POST/tickets/{id}/replytickets.replyAdd a staff reply
POST/tickets/{id}/closetickets.manageClose a ticket
POST/tickets/{id}/reopentickets.manageReopen a closed ticket
POST/tickets/{id}/prioritytickets.manageSet priority (low/medium/high)
DELETE/tickets/{id}tickets.deletePermanently delete

Tickets are created by players in-game via /ticket create <message>, not via the API. The API is staff-side only.

Worlds

MethodPathPermissionDescription
GET/worldsworlds.viewList loaded worlds
GET/worlds/{name}worlds.viewDetail for one world
POST/worlds/{name}/gameruleworlds.editSet a game rule
POST/worlds/{name}/timeworlds.editSet time of day (0–24000)
POST/worlds/{name}/weatherworlds.editSet weather (storm, thunder)
POST/worlds/{name}/difficultyworlds.editpeaceful / easy / normal / hard

Analytics

MethodPathPermissionDescription
GET/analytics/tps?hours=24analytics.viewTPS history
GET/analytics/sessions?hours=24analytics.viewPlayer session log

Files

The file editor is sandboxed to the server's plugins/ directory. Symlinks that escape are rejected by canonical-path validation. Editable extensions: .yml, .yaml, .json, .properties, .txt, .conf. Other files are listed but not editable. 1MB file size cap.

MethodPathPermissionDescription
GET/files/tree?path=...files.viewList a directory inside plugins/
GET/files/read?path=...files.viewRead a file (returns content + sha256 hash)
POST/files/writefiles.editWrite a file (requires expected_hash)

Writes use optimistic locking via the expected_hash field. If the file changed since you last read it, the server returns 409 Conflict with the current hash so you can re-read and decide whether to overwrite.

Write request

{
  "path": "Essentials/config.yml",
  "content": "...",
  "expected_hash": "sha256-of-the-content-when-you-last-read-it"
}

Conflict response (409)

{
  "error": "File changed since you last read it",
  "current_hash": "sha256-of-current-content"
}

Notifications (push)

Per-user, self-service. Used by the iOS app to register device tokens for APNs. No permission required beyond authentication.

MethodPathDescription
POST/devices/registerRegister an APNs device token for the current user
DELETE/devices/{token}Unregister a device token
GET/notifications/preferencesGet the user's notification preferences
PUT/notifications/preferencesUpdate preferences

Third-party integrations can ignore this section unless they're building their own iOS app.

Audit Log

MethodPathPermissionDescription
GET/audit?limit=100&before=<timestamp>OwnerAudit log entries, newest-first

Pagination is keyset-based: pass the timestamp of the oldest row from the previous page as before.

WebSocket

A single WebSocket connection delivers all real-time events.

ws://<server>:7070/api/v1/ws?token=<jwt>

The token is passed as a query parameter (WebSocket upgrades don't carry custom headers reliably across implementations). It's verified on connect; an invalid token closes the connection immediately with code 1008.

Envelope

Every server-to-client message has this envelope:

{
  "type": "event_type",
  "data": { ... },
  "timestamp": 1730390400000
}

timestamp is server-side milliseconds since epoch, applied at the moment the event was produced. Per-event fields go in data.

Event types

typedata fieldsDescription
systemmessageConnection-level message. Sent once on connect with "Connected to PulseConnect"
chatuuid, name, messageIn-game chat message
consolelineNew console log line. May include raw ANSI escape codes — clients can render them as colors or strip them
player_joinuuid, namePlayer joined the server
player_leaveuuid, namePlayer left the server
flagged_player_joinuuid, nameA player previously flagged by staff just joined. Sent in addition to player_join
ticket_createdticketId, playerName, subjectNew ticket opened
ticket_messageticketId, sender, senderType, messageMessage added to a ticket. senderType is "player" or "staff"
ticket_updateticketId, status?, priority?Ticket status or priority changed (close, reopen, priority change). Fields are present only if changed
tps_alerttps, messageTPS dropped below the configured threshold

Example payload

{
  "type": "chat",
  "data": {
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Notch",
    "message": "hello world"
  },
  "timestamp": 1730390400000
}

Client-to-server messages

Clients can send messages on the WebSocket, but the plugin currently only logs them — there are no client-initiated commands over the WebSocket. To execute a server command, use POST /command.

Permissions Catalog

The complete list of permission keys, used in PUT /auth/users/{username}/permissions:

Dashboard / Analytics

  • dashboard.view — see the dashboard at all
  • analytics.view — see TPS history and player session graphs

Server control

  • server.restart — restart the server
  • server.stop — stop the server dangerous
  • server.reload — reload the server config

Console

  • console.view — see live console logs and history
  • console.execute — run console commands (effectively grants OP-level access) dangerous

Players

  • players.view — see player list and details
  • players.kick — kick players
  • players.ban — ban / unban players
  • players.mute — mute / unmute players
  • players.edit_stats — set health, hunger, game mode
  • players.op — grant or revoke OP dangerous
  • players.inventory — view and edit player inventories
  • players.flag — flag players for staff attention

Tickets

  • tickets.view — see tickets
  • tickets.reply — reply to tickets
  • tickets.manage — close, reopen, set priority
  • tickets.delete — permanently delete tickets dangerous

Worlds

  • worlds.view — see world settings
  • worlds.edit — change game rules, time, weather, difficulty

Chat

  • chat.view — see in-game chat
  • chat.send — send messages as staff
  • chat.broadcast — send server-wide broadcasts

Files

  • files.view — list and read files in plugins/
  • files.edit — write files in plugins/ (full access to plugin configs) dangerous

Notification subscriptions

These don't unlock actions; they unlock receiving push notifications for specific events.

  • notifications.server_lag — receive a push when TPS drops
  • notifications.server_stop — receive a push when the server stops

Defaults

When the Owner creates a new staff user without specifying permissions explicitly, they get a view-only set:

dashboard.view, analytics.view, console.view, players.view, tickets.view, worlds.view, chat.view, files.view, notifications.server_lag, notifications.server_stop

The Owner toggles individual write permissions on per-staff-member.

Versioning

The API is versioned in the URL path (/api/v1/...). Breaking changes will go to /api/v2; /api/v1 will continue to work alongside.

Adding new endpoints, new optional fields, or new permission keys is not considered breaking. Removing or renaming endpoints, removing fields, or changing field types is.

Examples

Get current player count (curl)

TOKEN=$(curl -s -X POST http://localhost:7070/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"your-password"}' \
  | jq -r .token)

curl -s http://localhost:7070/api/v1/server/status \
  -H "Authorization: Bearer $TOKEN" \
  | jq .onlinePlayers

Send a broadcast from a Discord bot (Python)

import requests

base = "http://your-server:7070/api/v1"

# Login once at startup
token = requests.post(f"{base}/auth/login", json={
    "username": "discord-bot",
    "password": "..."
}).json()["token"]

headers = {"Authorization": f"Bearer {token}"}

# Send a broadcast
requests.post(f"{base}/command", headers=headers, json={
    "command": "say [Discord] Server restart in 5 minutes"
})

Watch chat in real time (Node)

const WebSocket = require("ws");

const ws = new WebSocket(`ws://your-server:7070/api/v1/ws?token=${TOKEN}`);

ws.on("message", (data) => {
  const event = JSON.parse(data);
  if (event.type === "chat") {
    console.log(`<${event.data.name}> ${event.data.message}`);
  }
});

Support