HTTP API reference
Every action the UI takes (start/stop a module, tail logs, edit config, manage ACLs and
XMPP accounts) goes through a plain JSON endpoint under /api/.... There is no separate
“external” API – the same endpoints the browser’s own JS calls are exactly what an
external caller (a script, another service, or another pyobs-web-admin instance acting as
a hub) calls too, authenticated the same way. See Hub mode for how authentication works
(X-Hub-Token vs. the browser session/CSRF cookie) and how to configure
HUB_CLIENTS/HUB_TOKEN for external callers.
Conventions
All request and response bodies are JSON; POST endpoints that take a body expect
Content-Type: application/json.
Responses are always a JSON object. Most write endpoints return {"success": true} or
{"success": false, "error": "..."} (a few older ones use {"ok": ...}/
{"error": ...} instead – called out below where that’s the case). A non-2xx status
usually accompanies the error case, but check the body’s own flag rather than only the
HTTP status.
404 means the path parameter (module/shared-config name) doesn’t exist; 502 means
a proxied call to another host failed; 400 means the request itself was invalid
(bad JSON, missing field, invalid ACL shape, …).
Host routing
Most endpoints below call the view layer’s _active_host helper, which resolves
request.session["active_host"] (defaulting to "localhost", i.e. this instance
itself) and, if it names a remote host, transparently proxies the whole request there
instead of answering locally. This is session state, so it only applies to browser
requests – an external caller authenticating via X-Hub-Token has no session, and
therefore always gets this instance’s own, local answer, regardless of what
HUB_HOSTS this instance may itself be configured with. To act on a different host in
the fleet, call that host’s own API directly (with a token it accepts) rather than relying
on this instance to route there for you.
A handful of write endpoints (api_acl’s POST, and the module-scoped ejabberd write
endpoints) instead accept an explicit "host" field in the JSON body, defaulting to
"localhost" when absent – this lets a fleet-wide page (or an external caller) target a
specific host regardless of session state. Where that’s supported, it’s noted below.
Fleet-wide endpoints (api_acl_matrix, api_comm_user_map) have no host-awareness at
all: they always answer for this instance’s own local modules only. Aggregating across a
whole fleet is done by whichever caller queries every host’s copy of these and merges the
results (this is what the hub’s own ACL matrix / Users pages do).
Status
Method |
Path |
Notes |
GET |
/api/statuses/
|
All modules. {"modules": [{"name", "status", "stats", "comm_user"}, ...]}.
stats is null unless status == "running", otherwise
{"pid", "cpu_percent", "memory_mb", "uptime_seconds"}. |
GET |
/api/modules/<name>/status/
|
One module. {"status": ..., "stats": ...|null}. |
Control
Method |
Path |
Notes |
POST |
/api/modules/<name>/start/
|
{"success": bool, "output": "..."}.
|
POST |
/api/modules/<name>/stop/
|
Same shape as start. |
POST |
/api/modules/<name>/restart/
|
Same shape as start. |
POST |
/api/modules/<name>/activate/
|
Enables the module (survives Start All); same response shape as start. |
POST |
/api/modules/<name>/deactivate/
|
Disables the module; same response shape as start. |
Logs
Method |
Path |
Notes |
GET |
/api/modules/<name>/logs/
|
Query params: lines (default 300, capped at 2000), filter (substring/regex
applied server-side), since (ISO-8601 instant – the time-range start date, so
only entries at or after it are returned), until (ISO-8601 instant – the
time-range end date, so only entries at or before it are returned), and before
(ISO-8601 instant – returns the last lines entries at or before it, for the log
pane’s scroll-to-top “load older logs” feature). since/until form a
[since, until] window; before (the page-back cursor) combines with until
as a second upper bound, the earlier of the two winning. before on its own is
journald-only (the file backend returns [] unless since is also set).
{"lines": [...]}. |
GET |
/api/modules/<name>/log-stats/
|
24h level counts. {"stats": {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}}. |
GET |
/api/logs/
|
Fleet-wide merged tail, independent of the active host. Query params: lines,
filter, modules – a comma-separated list of <host>:<module> tokens
(host is "localhost" or a HUB_HOSTS name); omit modules entirely for
every module on every configured host – plus since, until and before
(same time-range start/end date and “load older logs” semantics as the per-module
endpoint above, forwarded unchanged to each configured host). {"lines": [...],
"unreachable_hosts": [{"name", "error"}, ...]}; each line is tagged [host] when
more than one host is selected. |
GET |
/api/log-stats/
|
All modules’ 24h level counts on the active host. {"modules": {name: stats, ...}}. |
Config
Method |
Path |
Notes |
GET |
/api/modules/<name>/config/
|
{"content": "<raw YAML>"}.
|
POST |
/api/modules/<name>/config/
|
Body: {"content": "<raw YAML>"}. {"success": true} or
{"success": false, "error": "..."} (400 on bad JSON, 404 if the file disappeared
between request and write). |
GET / POST |
/api/shared/<name>/config/
|
Same shape as module config, for a shared YAML fragment under
How modules are managed. Not host-routed – shared config is always local. |
POST |
/api/modules/create/
|
Body: {"name": "..."}. {"success": true, "name": "..."} (400 if the name is
missing/invalid, 409 if a config with that name already exists). |
ACL
Method |
Path |
Notes |
GET |
/api/modules/<name>/acl/
|
Host-routed via the active host. {"acl": {...}|null, "source": "...", "error":
"..."|null}. |
POST |
/api/modules/<name>/acl/
|
Body: {"acl": {"allow": {caller: [methods]}, ...}|{"deny": [callers]}, "mode":
"enforce"|"log"} plus optional "host" (defaults to "localhost" – not
session-routed, see Host routing). acl may be null to clear a local
override. allow and deny are mutually exclusive. {"success": true} or
{"success": false, "error": "..."} (400 on a malformed acl, 409 on a save
conflict). |
GET |
/api/acl-matrix/
|
This instance’s own local ACL matrix only (no host-awareness) – the raw output of
services.build_acl_matrix(). Queried by a hub instance to fold this host into its
fleet-wide matrix; see ACL matrix and per-module ACL editing. |
Packages
Method |
Path |
Notes |
GET |
/api/packages/
|
{"packages": [{"name", "installed_version", "latest_version", "update_available",
"vcs"}, ...]} for every installed pyobs-* package (plus anything listed in
PYOBS_MANAGED_PACKAGES). latest_version is null and vcs is true
for a git/URL-installed package (no PyPI lookup is attempted for those).
|
POST |
/api/packages/<name>/update/
|
Upgrades one package via pip install --upgrade. {"ok": bool, "message":
"..."}. 404 if name isn’t actually installed (this endpoint refuses to install
an arbitrary new package). |
ejabberd – fleet-shared status/users (no host-awareness)
These answer using this instance’s own configured EJABBERD_API_URL/EJABBERDCTL
directly – they’re the delegation target another instance calls when its own
EJABBERD_HOST names this host (see ejabberd / XMPP integration), and are also what this
instance’s own dashboard calls when EJABBERD_HOST == "localhost".
Method |
Path |
Notes |
GET |
/api/ejabberd/status/
|
{"node_status", "registered_count", "online_count", "connected": [...]}.
|
GET |
/api/ejabberd/user/<user>/
|
{"comm_user", "registered", "sessions", "last", "ban_details"} for one bare XMPP
username.
|
GET |
/api/ejabberd/users/
|
{"users": [{"user", "connected", "session", "last", "ban_details"}, ...]} for
every registered account.
|
POST |
/api/ejabberd/user/<user>/register/
|
Body: {"password": "..."}. {"success": true} or {"error": "..."} (400). |
POST |
/api/ejabberd/user/<user>/change-password/
|
Body: {"password": "..."}. Same response shape as register. |
POST |
/api/ejabberd/user/<user>/ban/
|
Body: {"reason": "..."} (optional). Same response shape as register. |
POST |
/api/ejabberd/user/<user>/unban/
|
No body. Same response shape as register. |
POST |
/api/ejabberd/user/<user>/unregister/
|
No body. Same response shape as register. |
POST |
/api/ejabberd/user/<user>/kick/
|
Body: {"resource": "...", "reason": "..."} (resource required – the XMPP
resource of the session to disconnect). Same response shape as register. |
ejabberd – browser-facing (fleet-shared, EJABBERD_HOST-routed)
Unlike the endpoints above, these delegate to wherever EJABBERD_HOST points and answer
the same regardless of which host’s page called them – see ejabberd / XMPP integration.
Method |
Path |
Notes |
GET |
/api/ejabberd-summary/
|
{"enabled": false} if EJABBERD_ENABLED is off, else {"enabled": true,
"node_status", "registered_count", "online_count", "connected"}.
|
GET |
/api/xmpp-users/
|
{"enabled": false} or {"enabled": true, "users": [...]}} (same per-user shape
as /api/ejabberd/users/).
|
GET |
/api/comm-user-map/
|
This instance’s own local comm.user -> modules map (no host-awareness).
{"map": {user: [{"name", "status"}, ...]}}. |
POST |
/api/xmpp-users/<user>/register/
|
Body: {"password": "..."} (required, no config to read a default from here).
{"success": bool, "error": "..."}. |
POST |
/api/xmpp-users/<user>/ban/
|
Body: {"reason": "..."} (optional). Same response shape as register. |
POST |
/api/xmpp-users/<user>/unban/
|
No body. Same response shape as register. |
POST |
/api/xmpp-users/<user>/unregister/
|
No body. Same response shape as register. |
POST |
/api/xmpp-users/<user>/kick/
|
Body: {"resource": "...", "reason": "..."} (resource required). Same response
shape as register. |
Module-scoped ejabberd (session/host-routed)
Per-module identity view/actions, resolving the module’s own comm.user first and then
delegating the actual ejabberd call per EJABBERD_HOST – see ejabberd / XMPP integration.
Method |
Path |
Notes |
GET |
/api/modules/<name>/ejabberd/
|
{"comm_user": null} if the module has none, else {"comm_user",
"module_running", "shared_with", "registered", "ban_details"} plus "sessions"/
"last" only when module_running is true.
|
POST |
/api/modules/<name>/ejabberd/register/
|
No body needed (uses the module’s own configured comm.password). Accepts an
optional "host" field (see Host routing). {"success": bool, "error":
"..."}. |
POST |
/api/modules/<name>/ejabberd/change-password/
|
No body; generates and writes back a fresh password to every local module sharing
that identity. Accepts an optional "host" field. {"success": bool,
"updated_modules": [...], "error": "..."}. |
POST |
/api/modules/<name>/ejabberd/ban/
|
Body: {"reason": "..."} (optional). Host-routed via the active host (no
"host" field override). {"success": bool, "error": "..."}. |
POST |
/api/modules/<name>/ejabberd/unban/
|
No body. Same response shape as ban. |
POST |
/api/modules/<name>/ejabberd/unregister/
|
No body. Same response shape as ban. |