API reference
GraphQL for almost everything, REST for the few things that are not, WebSocket for live state, MQTT for devices.
Authentication
Sign in with username and password; you get a signed HS256 JWT to send as a bearer token on every
subsequent request. Tokens last roughly ten hours and there is no refresh token — re-authenticate on a
401.
curl -s -X POST https://your-host/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"..."}'
# → { "username": "admin", "roles": ["ROLE_ADMIN"], "access_token": "eyJ..." }curl -s https://your-host/graphql \
-H 'Authorization: Bearer eyJ...' \
-H 'Content-Type: application/json' \
-d '{"query":"{ me { id username } }"}'GraphQL
POST /graphql, authenticated. In development a GraphiQL explorer is served at
/graphiql, which is the fastest way to discover the schema — it is generated partly from
your own domain model.
Auto-generated CRUD
Domain classes that declare a GraphQL mapping get queries, paginated list queries and create / update / delete mutations generated for them. That covers devices, ports, peripherals, cables, zones, layers, racks, patch panels, jobs, scenarios, users, roles, categories and configuration entries.
query {
devicePeripheralList(max: 20, sort: "name") {
id
name
category { name }
zones { id name }
connectedTo { id internalRef state value }
}
}Entities without one are skipped by the schema factory, so no auto-CRUD exists for them. The shared-link audit table works this way: rows can be read through a purpose-built query but never created, altered or deleted through the API.
Custom queries
| Query | Returns |
|---|---|
me | The current user's id and username. |
navigation | Menu and breadcrumb structure for the SPA. |
appConfigList | Every configuration key. admin — includes secrets. |
uiConfigList | The UI-safe subset only. What regular users get at boot. |
config(key) | A single configuration value. |
qrConfig | QR label settings and the variables available in the template. |
myMessages(level, state) | The signed-in user's inbox. |
myUnreadCount | Unread message count for the badge. |
myNotificationRules | The user's own mute rules. Owner-scoped. |
pushPublicKey | VAPID public key, empty when push is unconfigured. |
sprinklerScheduleJobs | Jobs bound to sprinkler peripherals, for the scheduler grid. |
jobExecutionHistoryByJobId(jobId, limit) | Recent runs of a job. |
sharedWidgets(state) | All guest links. admin |
sharedWidgetAudit(sharedWidgetId, count, offset) | Attempt history, newest first. Defaults to 200. |
discoveredDevices | UDP-broadcast scan for MegaD controllers on the LAN. |
deviceBackupList(deviceId) | Stored controller configuration backups. |
deviceFetchIpFromMqtt(deviceCode) | A device's current IP straight from the broker, without needing a port row to exist. |
userRolesForUser(userId) | Role assignments. |
cache / cacheAll | Hazelcast cache inspection. |
deviceModelList | Supported device models. |
Custom mutations
| Mutation | Does |
|---|---|
pushEvent(input) | Publishes an event onto the bus — the generic way to switch a peripheral, port or zone. |
voiceCommand(transcript, locale, sessionId) | Runs a natural-language command through the agent. |
mowerCommand(deviceId, action) | START / STOP / PAUSE / RESUME / DOCK. |
jobTrigger(jobId) | Runs a job now. |
jobSchedule / jobUnschedule | Adds or removes a job from the scheduler. |
scenarioDelete(id) | Deletes a scenario. |
appConfigUpdate(key, value, commitMessage) | Writes a configuration key and commits it to the repository. |
qrConfigUpdate(...) | Updates QR label settings. |
publishMqtt(topic, payload) | Publishes an arbitrary MQTT message — what the explorer's publish form calls. |
messageUpdateState / messageBatchUpdateState | Marks messages read or archived. |
notificationRuleCreate / notificationRuleDelete | Mute rules. |
pushSubscribe / pushUnsubscribe | Web Push registration. |
sharedWidgetCreate(input) | Creates a guest link, returning its token. |
sharedWidgetUpdateState(id, state, ...) | Disable, archive or re-enable a link. |
deviceInitFromController(ip, password) | Imports a controller's port layout from the device itself. |
deviceBackupConfig(deviceId) | Stores the controller's configuration in the database. |
deviceRestoreToController / deviceSyncFromBackup | Push a backup back to hardware, or reconcile the database from one. |
navimowOAuthStart / navimowOAuthComplete | The mower cloud sign-in flow. |
meUpdateLanguage / meUpdateTimezone | Per-user preferences. |
userRolesSave(input) | Assigns roles. |
cacheDelete(cacheName, cacheKey) | Evicts a cache entry. |
Switching something
Control does not go through an auto-CRUD update on the port row — it goes through the event bus, so zone expansion, device confirmation and audit logging all apply.
mutation {
pushEvent(input: {
p0: "evt_switch"
p1: "PERIPHERAL"
p2: "1042"
p3: "api"
p4: "ON"
}) { success }
}Use p1: "ZONE" to switch everything in an area, recursively, or p1: "PORT" to
address a single I/O line.
Voice
mutation {
voiceCommand(transcript: "turn off everything on the terrace",
locale: "en", sessionId: "abc123") {
reply
awaitingReply
sessionId
audioContent # base64 MP3, when neural TTS is enabled
}
}Keep the returned sessionId and pass it back to continue a conversation.
awaitingReply is true only when nothing was actioned and the reply is a question — the
client uses it to reopen the microphone.
REST endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/login | none | Obtain a JWT. |
GET | /api/me | user | Current user. |
GET | /api/public/share/:token | anonymous | Guest link metadata. |
POST | /api/public/share/:token/verify-pin | anonymous | Check a PIN. |
POST | /api/public/share/:token/action | anonymous | Operate the shared peripheral. |
GET | /api/public/event, /pub-event, /e | LAN only | Device-initiated event endpoints. |
GET | /api/labels/cable/:id | user | Printable cable label. |
GET | /api/labels/device/:id, /api/labels/peripheral/:id | user | Device and peripheral labels. |
GET/PUT | /api/users/:id/avatar | user | Avatar image (small PNG/JPEG). |
GET/PUT | /api/screens/:id/background | admin | Floor-plan background image. |
POST | /api/screens/:id/background/resize | admin | Resize a background in place. |
POST | /api/screens/import-svg | admin | Import a legacy SVG floor plan. |
GET | /auth/external/callback | public path | Navimow OAuth callback. |
GET | /actuator/healthcheck, /actuator/info, /actuator/prometheus | open | Health, build info, metrics. |
The device event endpoints assume a LAN and are gated by CIDR, not by a credential — refuse them
from outside at your proxy. The public share endpoints have no rate limiting; so does
/api/login. Add both at the proxy.
WebSocket
Live state arrives over STOMP on WebSocket — not GraphQL subscriptions. Clients authenticate, subscribe, and receive port-value and device-status changes as they happen, which is why two phones and a wall tablet stay in agreement without polling.
The public shared-link pages are the exception: no authenticated socket, so they re-read state after each action.
MQTT
The device-facing API. myHAB subscribes to myhab/# (plus the ONVIF bridge prefix) and
routes inbound messages by matching them against per-model patterns. Full topic contracts are in
Integrations → MQTT topic contracts.
# command a relay
mosquitto_pub -t 'myhab/esp_terrace/relay/3/cmd' -m 'ON'
# watch everything
mosquitto_sub -t 'myhab/#' -v
A useful side door: any source can raise an in-app message by publishing to
myhab/<source>/notify. That is how a script on another machine can put something in
the inbox without touching the HTTP API.
Errors and conventions
- GraphQL mutations return a response object with a success flag and messages rather than throwing for expected failures.
- REST errors use conventional status codes:
400malformed,401unauthenticated,403not permitted or an unusable state,404unknown. - Timestamps are milliseconds since epoch, UTC.
- Ids are strings in GraphQL (
ID) even though they are numeric in the database. - Publishing an event is fire-and-forget: a success means the command was accepted and published, not that the device confirmed it.