Public API Reference
Vexillum has no bundled public dashboard - the reflector ships as an API-only public surface. This page describes the public, unauthenticated JSON REST + Server-Sent Events (SSE) API for reading reflector status data: instance inventory, per-mode runtime stats, connected clients, and recent call history. It’s intended for building independent dashboards or integrations against a running Vexillum node.
Status
Section titled “Status”| Field | Value |
|---|---|
| API status | Beta |
| Contract version | v1 |
GET / redirects to the interactive docs described below rather than serving a
page. (An earlier Beta build shipped a server-rendered HTML dashboard at / and
/public/<mode>/<instance>; it was retired once this API existed, since it
added nothing a caller of this API couldn’t already get for itself.)
Interactive docs: /api/public/v1/docs serves a Swagger UI browser against
the machine-readable spec at /api/public/v1/openapi.json (hand-maintained
OpenAPI 3.0 - see the source-of-truth note under Endpoints below for what it
does and doesn’t capture). This page remains the fuller narrative reference, in
particular for SSE framing details OpenAPI can’t fully express.
Base URL and versioning
Section titled “Base URL and versioning”All endpoints are served under:
The v1 segment is a stable contract version. Breaking changes (removed fields,
changed types, changed semantics) will ship under a new version prefix rather
than altering v1 in place - but as Beta software, the set of fields may
still grow and minor behavioral details may be refined before a 1.0 stability
commitment.
Access
Section titled “Access”-
No authentication. This API only ever exposes read-only reflector status data - nothing scoped to an admin account.
-
CORS is an operator-configured allowlist, off by default. Configure it in the reflector’s config file, all under one
[public_api]section (see Configuration):enabled/listenare the same section as the CORS/rate-limit settings since there’s only one public-facing listener - setenabled = falseto disable the public API entirely.With
cors_allowed_originsempty (the default), the API is same-origin/ non-browser only -curl, server-to-server calls, and same-origin pages are unaffected; only cross-origin browser JS is blocked from reading the response. List the exact origin(s) (scheme://host[:port], no path) your dashboard is served from to allow it, or use the literal"*"entry to allow any origin. -
Rate limiting is on by default - 120 requests/minute per client IP, burst 20, shared across every route in this API (REST, SSE, and the docs/spec routes). Exceeding it returns
HTTP 429with aRetry-Afterheader and a JSON{"error": "...", "status": 429}body. Setrate_limit_per_minute = 0to disable it, or raise/lower the numbers to taste. Per-IP identification uses the same trusted-proxy configuration as the rest of the server (VEX_ADMIN_TRUSTED_PROXIES) - if you’re behind a reverse proxy, make sure that’s configured correctly or every request will appear to come from the proxy’s IP and share one budget.
Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
| GET | /api/public/v1/overview |
One-shot snapshot of all instances across all modes. |
| GET | /api/public/v1/instances/:mode/:instance |
One-shot snapshot of a single instance. |
| GET | /api/public/v1/liveness |
One-shot supervisor liveness snapshot, all instances. |
| GET | /api/public/v1/liveness/:mode/:instance |
One-shot supervisor liveness snapshot of a single instance. |
| GET | /api/public/v1/activity |
One-shot aggregated live calls + last-heard, all instances. |
| GET | /api/public/v1/events/overview |
SSE stream of the overview snapshot. |
| GET | /api/public/v1/events/instances/:mode/:instance |
SSE stream of a single instance’s snapshot. |
| GET | /api/public/v1/events/activity |
SSE stream of the aggregated activity feed. |
| GET | /api/public/v1/openapi.json |
Machine-readable OpenAPI 3.0 spec (see note below). |
| GET | /api/public/v1/docs |
Swagger UI browser for the spec above. |
:mode is one of dstar, dmr, m17, nxdn, p25, ysf, vafm
(case-insensitive). :instance is the instance name as configured on the
reflector (case-insensitive lookup; the response always reports the canonical
stored casing).
Note on openapi.json: it is hand-maintained, not generated from the Go
handlers, so treat this page and the actual server behavior as the source of
truth if the two ever disagree. It also collapses per-mode differences in the
runtime/clients shapes (documented below) into generic, loosely-typed
schemas - the mode-specific field lists here are more precise than what the spec
can express without seven near-duplicate schemas.
GET /api/public/v1/overview
Section titled “GET /api/public/v1/overview”Returns totals across all instances, a per-mode summary, and a flat list of every configured instance with its current state.
detail_path is a convenience - it’s exactly what you’d build yourself from
mode/instance via /api/public/v1/instances/{mode}/{instance} (below).
active_calls and last_heard are a bounded “at a glance” preview (up to 5
entries each, newest first) - enough for a dashboard’s main instance list to
show who’s on without a per-instance drill-down. active_call_count is the
true total, which can exceed the preview length if more calls are active than
are being previewed. For the fuller per-instance history (up to 200 entries) see
the instance-detail endpoint below; for a genuinely merged feed across every
instance, see the activity endpoint.
GET /api/public/v1/instances/:mode/:instance
Section titled “GET /api/public/v1/instances/:mode/:instance”Returns detailed runtime state, stats, connected clients, and recent call
history for one instance. The mode-specific block (dmr, dstar, m17,
nxdn, p25, ysf, or vafm) is only present for the instance’s actual mode;
the other six are omitted.
Not found
Section titled “Not found”If the instance doesn’t exist, the response is HTTP 404 with found: false:
Redirects
Section titled “Redirects”If :instance resolves to a configured instance but doesn’t exactly match its
canonical stored name, the response is HTTP 307 with a Location header
pointing at the canonical /api/public/v1/instances/... resource. Follow it
like any other HTTP redirect (fetch and curl -L do this automatically).
GET /api/public/v1/liveness and /liveness/:mode/:instance
Section titled “GET /api/public/v1/liveness and /liveness/:mode/:instance”A lean, monitoring-focused view of instance state - purpose-built for
third-party availability checkers (think uptime monitors, status-page
integrations, or a directory site polling to confirm a listed reflector is
actually up) rather than for building a dashboard. It’s deliberately decoupled
from /overview and /instances/:mode/:instance above: those endpoints can
grow new dashboard-oriented fields over time without changing what a monitoring
integration parses here.
Bulk (/liveness):
Per-instance (/liveness/:mode/:instance) returns one entry in the same shape,
plus found/message, and 404s with found: false if the instance doesn’t
exist - same not-found and canonical-name-redirect behavior as the instance-
detail endpoint above.
reason is the last recorded state transition for that instance - e.g.
"started", "restarted", "stopped", "disabled", or failure detail when
state is "failed". It’s omitted when empty.
state_changed_at is when that transition was recorded - present regardless of
state, useful for flapping/staleness detection even when not running.
uptime_seconds is derived from it and only appears when state is
"running". Both reset whenever the Vexillum process itself restarts -
reflector instances run in-process rather than as separate child processes, so
there’s no independent reflector uptime to preserve continuity against; if
Vexillum is down, everything it hosts is down too, and “since when running”
restarts along with it.
GET /api/public/v1/activity
Section titled “GET /api/public/v1/activity”A genuinely merged, cross-instance feed - for a real “live call wall” view, as
opposed to the per-instance previews in the overview response above. Every
instance/mode’s active calls and last-heard history are tagged with mode/
instance and merged into two flat, time-ordered lists.
active_calls is sorted by started_at descending and capped at 200 entries
(defensive - in practice bounded by however many calls are actually active
system-wide). last_heard is sorted by capture time descending and capped at 50
entries across the whole system. Like the overview endpoint, hitting /activity
(or its SSE counterpart) also drives last-heard capture for every instance - you
don’t need to have called /instances/:mode/:instance first for an instance’s
activity to show up here.
SSE streams
Section titled “SSE streams”/api/public/v1/events/overview,
/api/public/v1/events/instances/:mode/:instance,
and /api/public/v1/events/activity all use the same framing:
- On connect, the current snapshot is sent immediately.
- The snapshot is re-sent every 2 seconds.
- Every 20 seconds, a
: heartbeatcomment line is sent to keep idle connections alive through proxies. - Each snapshot is one
event: snapshotSSE event whosedata:payload is the same JSON shape as the corresponding REST endpoint above.
The instance-events stream additionally sets redirect_path, mode, and
instance (to the canonical values) instead of the normal payload when the
requested instance needs to be resolved to a different canonical name - since
SSE can’t send an HTTP redirect mid-stream, clients should treat a non-empty
redirect_path as “resubscribe using mode/instance from this payload.”
Example client (JavaScript):
Beta notes
Section titled “Beta notes”CORS and rate limiting are configurable (see Access above) with production- appropriate defaults (CORS locked down, rate limiting on). What’s still Beta: the exact set of fields on each mode’s runtime/stats/client blocks may grow as new modes gain features, and minor response-shape details may still be refined before a 1.0 stability commitment. If you’re integrating against this API, expect to tolerate new fields appearing over time.