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 }
  }
}
Absence of a mapping is a deliberate boundary.

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

QueryReturns
meThe current user's id and username.
navigationMenu and breadcrumb structure for the SPA.
appConfigListEvery configuration key. admin — includes secrets.
uiConfigListThe UI-safe subset only. What regular users get at boot.
config(key)A single configuration value.
qrConfigQR label settings and the variables available in the template.
myMessages(level, state)The signed-in user's inbox.
myUnreadCountUnread message count for the badge.
myNotificationRulesThe user's own mute rules. Owner-scoped.
pushPublicKeyVAPID public key, empty when push is unconfigured.
sprinklerScheduleJobsJobs 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.
discoveredDevicesUDP-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 / cacheAllHazelcast cache inspection.
deviceModelListSupported device models.

Custom mutations

MutationDoes
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 / jobUnscheduleAdds 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 / messageBatchUpdateStateMarks messages read or archived.
notificationRuleCreate / notificationRuleDeleteMute rules.
pushSubscribe / pushUnsubscribeWeb 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 / deviceSyncFromBackupPush a backup back to hardware, or reconcile the database from one.
navimowOAuthStart / navimowOAuthCompleteThe mower cloud sign-in flow.
meUpdateLanguage / meUpdateTimezonePer-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

MethodPathAuthPurpose
POST/api/loginnoneObtain a JWT.
GET/api/meuserCurrent user.
GET/api/public/share/:tokenanonymousGuest link metadata.
POST/api/public/share/:token/verify-pinanonymousCheck a PIN.
POST/api/public/share/:token/actionanonymousOperate the shared peripheral.
GET/api/public/event, /pub-event, /eLAN onlyDevice-initiated event endpoints.
GET/api/labels/cable/:iduserPrintable cable label.
GET/api/labels/device/:id, /api/labels/peripheral/:iduserDevice and peripheral labels.
GET/PUT/api/users/:id/avataruserAvatar image (small PNG/JPEG).
GET/PUT/api/screens/:id/backgroundadminFloor-plan background image.
POST/api/screens/:id/background/resizeadminResize a background in place.
POST/api/screens/import-svgadminImport a legacy SVG floor plan.
GET/auth/external/callbackpublic pathNavimow OAuth callback.
GET/actuator/healthcheck, /actuator/info, /actuator/prometheusopenHealth, build info, metrics.
Two surfaces need protecting at the edge.

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