Skip to main content

REST API Reference

The NeoMind backend serves a REST API on Axum. This page is an integrator's overview: OpenAPI spec and tooling, base URL, auth, unified response format, and endpoint groups by business domain.

Entry Points

ItemValue
Base URLhttp://<SERVER_IP>:9375/api
Interactive API consolehttp://<SERVER_IP>:9375/api/docs (Scalar — browse and debug)
OpenAPI 3 specGET /api/docs/openapi.json (354 operations, 278 paths, 136 schemas)
Machine-readable route list (with auth classes)GET /api/docs/routes.json
Endpoint definitions (source)crates/neomind-api/src/server/router.rs
Default port9375 (override with --port or the NEOMIND_PORT env var)

Every endpoint path starts with /api. The endpoint lists below omit the /api prefix.

OpenAPI Spec & Tooling

As of 0.9.24 the OpenAPI 3.0 spec covers every REST operation — 334/338 handlers carry full annotations (the other 4 are wildcard routes; see "Honest limits" below), all 136 schemas are registered and every one of the 135 $refs resolves. You can import the spec into any standards-compliant tool or generate a typed client directly.

Download the spec

curl -o neomind-openapi.json http://<SERVER_IP>:9375/api/docs/openapi.json

~230 KB. A CI drift test guards it: every annotated path must actually exist in the router, so the spec cannot silently rot.

The online console (Scalar)

Open /api/docs in a browser — nothing to install:

  1. The left tree groups endpoints by domain tag (devices, rules, agents, dashboards … 37 groups); expand any endpoint to see its parameters, request-body schema and documented response codes;
  2. Click an endpoint → Try it out → fill in parameters/body → Execute; the real response (headers and timing included) appears on the right;
  3. Auth: the spec itself embeds no security definitions (auth classes are in the table below; the per-endpoint authority is routes.json). When debugging protected endpoints, add the header manually — in the console's parameter area add X-API-Key: <your key> (or Authorization: Bearer <jwt>) to the request;
  4. The console's top-right corner exports the spec file (OpenAPI JSON).

Import into Apifox / Postman

Apifox:

  1. Project settings → Import Data → choose OpenAPI/Swagger;
  2. Use URL mode with http://<SERVER_IP>:9375/api/docs/openapi.json (or the file downloaded above);
  3. All endpoints, request-body structures and enums arrive ready; put X-API-Key into an Apifox environment variable for bulk auth.

Postman: Import → paste the URL or drop the file — same result.

Generate a typed client

# Once
npm install @openapitools/openapi-generator-cli -g

# TypeScript + axios
openapi-generator-cli generate \
-i neomind-openapi.json -g typescript-axios -o src/api-generated

# Python
openapi-generator-cli generate \
-i neomind-openapi.json -g python -o ./neomind-client

The output includes per-endpoint functions and full request/response typings (enums and required constraints included). React projects can use orval (npx orval --input neomind-openapi.json --output src/api.ts --client axios) to generate React Query hooks.

Auth-class quick reference (from routes.json)

ClassMeaningHeader
publicNo authnone
jwt-or-api-keyWeb session or API KeyX-API-Key: <key> or Authorization: Bearer <jwt>
jwt-onlyAdmin JWT only — API Keys not acceptedAuthorization: Bearer <jwt>
webhook / ws / debugSpecial channels (signature check / WebSocket upgrade)see the endpoint

Unsure about an endpoint? Every entry in GET /api/docs/routes.json carries method, path and auth.

Honest limits (what the spec does not contain)

  • 4 wildcard routes (GET /api/images/*path, GET /api/docs/*rest, GET /api/extensions/:id/assets/*asset_path, ANY /api/share/:token/proxy/*path) cannot be expressed as OpenAPI path templates — they exist only in routes.json;
  • Auth metadata is not embedded in the spec (use the table above);
  • Long-running endpoints (builtin-llm/download, upload-model, import-local) can hold a connection for minutes to tens of minutes — give generated clients a dedicated timeout.

Authentication

Two schemes:

1. JWT (User Session)

Default for the Web UI:

POST /api/auth/login    { "username": "...", "password": "..." }
→ returns a JWT
Subsequent requests add the header:
Authorization: Bearer <jwt>

2. API Key (Programmatic)

For scripts / 3rd-party integrations:

  • Generate a key under Settings → API Keys
  • Send the header on every request:
X-API-Key: <key>

API Keys are independent of user sessions and can be scoped and set to expire.

Public Endpoints (No Auth)

A handful of endpoints are open:

  • /api/health / /health/status / /health/live / /health/ready
  • /api/metrics (Prometheus text-format runtime metrics: HTTP request counters, EventBus dropped events, uptime and version — point a Prometheus scraper at it)
  • /api/system/network-info
  • /api/auth/status / /auth/verify
  • /api/auth/login / /auth/register
  • /api/setup/* (first-run wizard)
  • /api/llm-backends/types
  • /api/messages/channels/types
  • /api/extensions
  • /api/capabilities / /capabilities/:name
  • /api/tools

Unified Response Format

Success

{
"success": true,
"data": { /* business payload */ },
"meta": { /* optional: pagination / count / timestamps */ }
}

Integration note: the CLI wraps an extra data layer — integrators extracting from data.data should be aware. The raw HTTP response is as shown above.

Failure

{
"success": false,
"error": {
"code": "DEVICE_NOT_FOUND",
"message": "Device with id 'xxx' not found"
}
}

HTTP status codes follow convention: 4xx client errors, 5xx server errors. Pull the human-readable text from error.message.

Field Naming Convention

Important gotcha: the backend returns snake_case (e.g. data_source), the frontend uses camelCase (e.g. dataSource). The frontend converts every API response via web/src/store/persistence/types.ts::fromDashboardDTO(). When you parse the JSON yourself as an integrator, trust the backend's snake_case.

This page targets integrators and script authors. UI-level operations live in the User Guide.

Main Endpoint Groups

Auth

MethodPathDescription
POST/auth/loginLogin, get JWT
POST/auth/registerSelf-service registration (disabled by default; an admin can enable it in settings. Registrants get the regular user role — the first admin is created via /setup/initialize)
GET/auth/statusCurrent auth status
GET/auth/verifyVerify JWT validity

Devices

MethodPathDescription
GET/devicesList devices
POST/devicesCreate device (requires connection_config: {} even if empty)
GET/devices/:idDevice detail (metrics + commands; status is three-state: online / offline (seen before, timed out) / disconnected (never seen))
GET/devices/:id/currentCurrent values for all device metrics
PUT/devices/:idUpdate device. offline_timeout_secs tri-state: absent = keep current, null = clear override (fall back to template/global), number = set (30–86400 seconds)
DELETE/devices/:idDelete device
GET/devices/:id/telemetryDevice telemetry history — see Telemetry query contract
GET/telemetryCross-device telemetry query (?source=&metric=&start=&end=&limit=&offset=; offset skips the newest N items for server-side pagination; the response carries an exact total_count)
POST/devices/:id/command/:commandSend command (body is the params object, e.g. {"offset": 1})
POST/devices/:id/webhookPush data via webhook (no auth)
GET/device-typesList device types
POST/device-typesCreate device type
GET/devices/draftsPending drafts (auto-discovered)
POST/devices/drafts/:id/approveApprove a draft

Telemetry query contract

Query parameters for GET /devices/:id/telemetry (timestamps are always Unix seconds):

ParameterSemantics
metricA specific metric; omit for all metrics of the device
start / endTime window (seconds). hours=N (1–720) derives the window when start is absent (honored since 0.9.24; previously ignored)
aggregateavg / min / max / sum / lastthe value field reflects the requested function (since 0.9.24; previously always avg); unknown values return 400; the raw fields (min/max/sum/count) always ride along
limitPoints per page, 1–5000, default 100
offsetSkip the newest N points (offset pagination)
cursorCursor pagination: the previous page's oldest timestamp; the next page returns strictly older points (no boundary duplicates). pagination.next_cursor being null in the response means last page (short-page signal) — stop paginating
history=trueAccess to a deleted device's archived data — an unknown device 404s here (consistent with /devices/:id); this flag reads the archive
bucketedServer-side downsampling; returns at most limit evenly-spaced points for charts

Polling note for integrators: when a device is deleted by another client (CLI, second session), this endpoint changes from 200+empty to 404 — pollers should handle 404 and stop polling that device, or switch to history=true for the archive.

GET /telemetry (cross-device) additionally supports count for aggregate; unknown values are likewise a 400.

Dashboards

MethodPathDescription
GET/dashboardsList dashboards
POST/dashboardsCreate dashboard
GET/dashboards/:idDashboard detail
PUT/dashboards/:idUpdate dashboard (including layout)
DELETE/dashboards/:idDelete dashboard
POST/dashboards/:id/shareGenerate a share link (with expiration)
GET/share/:tokenAccess a shared dashboard (no auth)

Rules

MethodPathDescription
GET/rulesList rules
POST/rulesCreate rule — JSON body (name / trigger / condition / actions)
GET/rules/:idRule detail
PUT/rules/:idUpdate rule
DELETE/rules/:idDelete rule
POST/rules/:id/testDry-run the rule (no real action)

Rules use JSON format (not a DSL string). Example POST body:

{
"name": "High Temp Alert",
"trigger": { "trigger_type": "data_change" },
"condition": { "condition_type": "comparison", "source": "device:sensor-01:temperature", "operator": "greater_than", "threshold": 30 },
"actions": [ { "type": "notify", "message": "Too hot" } ]
}

Condition types: comparison / range / logical. Action types: notify / execute / trigger_agent. Trigger types: data_change / schedule / manual.

Agents

MethodPathDescription
GET/agentsList agents
POST/agentsCreate agent
GET/agents/:idAgent detail
PUT/agents/:idUpdate agent
DELETE/agents/:idDelete agent
POST/agents/:id/statusControl execution (body {"status": "active"} / "paused")
GET/agents/:id/executionsExecution history

Required fields for create: user_prompt (required), schedule: {"schedule_type": "..."} (required). When no resources are bound, also set execution_mode: "free".

LLM Backends

MethodPathDescription
GET/llm-backendsList backends
POST/llm-backendsAdd backend (Ollama / OpenAI / Anthropic / …)
GET/llm-backends/:idBackend detail (includes probed capabilities)
PUT/llm-backends/:idUpdate backend
DELETE/llm-backends/:idDelete backend
PATCH/llm-backends/:id/capabilitiesManually override capability (body {"multimodal": true} / false / null, null clears)

Messages

MethodPathDescription
GET/messagesMessage list
GET/messages/channelsList notification channels
POST/messages/channelsAdd a channel (webhook/email/telegram/wecom/dingtalk/slack/feishu)
PUT/messages/channels/:nameUpdate channel
DELETE/messages/channels/:nameDelete channel
POST/messages/channels/:name/testTest channel delivery
POST/messagesSend a message manually

Extensions

MethodPathDescription
GET/extensionsList installed extensions
GET/extensions/typesEnumerate extension types
POST/extensions/syncScan the extensions directory and install (sync)
GET/extensions/:idExtension detail
GET/extensions/:id/healthHealth check
GET/extensions/:id/commandsList extension commands
POST/extensions/:id/commandExecute an extension command
GET/extensions/:id/componentsDashboard components provided by the extension

Marketplace component install (POST /frontend-components/market/install, since 0.9.24): failures return real 4xx/5xx (component not found, marketplace unreachable, …) instead of HTTP 200 wrapping success:false — clients branching on status codes are now reliable. | GET / WS | /extensions/:id/stream | Extension stream session (Push-mode real-time frames; see Realtime API) |

Data Push

MethodPathDescription
GET/data-pushList push targets
POST/data-pushCreate a push target (Webhook or MQTT)
GET/data-push/:idDetail
PUT/data-push/:idUpdate
DELETE/data-push/:idDelete
POST/data-push/:id/testPush once as a test
POST/data-push/:id/startStart
POST/data-push/:id/stopStop
GET/data-push/:id/logsDelivery logs

Storage & System

MethodPathDescription
GET/settings/*System settings (retention policy, etc.)
GET/system/network-infoNetwork info (MQTT / webhook endpoints)
GET/metricsPrometheus text metrics (public): HTTP request counters, uptime, event-bus drop counters

Realtime API

In addition to REST, NeoMind exposes:

  • WebSocket: ws://<host>:9375/api/events/ws — dashboard live data, device state changes
  • SSE: GET /api/events/stream (Server-Sent Events) — same event stream over plain HTTP
  • MQTT: connect directly to mqtt://<host>:1883 and subscribe to device topics

Extension Stream (/api/extensions/:id/stream)

Push-mode extensions (video/audio and other continuous-frame outputs) establish a stream session through this WebSocket endpoint. Since 0.9.23, optional binary push frames are supported:

  1. The client opts in by sending {"binary": true} in the init config
  2. The server confirms via session_created.binary; if unconfirmed, the legacy Text (JSON + base64) format is kept
  3. Once enabled, push_output frames travel as WS Binary frames, avoiding double base64 encoding overhead. Frame format:
[kind u8=1][version u8=1][sequence u64 BE][meta_len u32 BE][meta JSON][payload bytes]

meta mirrors the Text envelope fields (minus data/sequence); control messages (session_created, error, etc.) always travel on Text frames — the WS frame type is the first-level discriminator. Any combination of old/new frontends and old/new servers degrades safely.

The canonical reference for the realtime protocol (WebSocket / SSE) is the frontend implementation: web/src/lib/events.ts and web/src/lib/websocket.ts.

Error Handling Example

import requests

resp = requests.post(
"http://host:9375/api/devices",
json={"name": "sensor", "device_type": "temp", "connection_config": {}},
headers={"X-API-Key": KEY},
)
data = resp.json()
if not data.get("success"):
err = data["error"]
print(f"[{err['code']}] {err['message']}")
else:
device = data["data"]

Next Steps

  • Full endpoint list: crates/neomind-api/src/server/router.rs — the authoritative route definitions (public / protected / admin)
  • Adding a new endpoint → add a handler under crates/neomind-api/src/, follow the existing per-module pattern
  • Realtime push → WebSocket / SSE (see web/src/lib/websocket.ts)

Last updated: 2026-09-14