WebSocket API
The WebSocket API is the primary real-time interface used by the frontend and the ApexConnect companion apps. It lives at:
ws://apexos.local:1702/api/websocket
The endpoint itself does not require HTTP authentication — authentication happens as the first phase of the WebSocket conversation.
Connection lifecycle
1. Auth phase
Immediately after the socket opens, the server sends:
{"type": "auth_required", "apexos_version": "2026.7.1"}
(apexos_version carries the core version string.)
The client must reply within 10 seconds with an auth message carrying an access token — either a short-lived token obtained through the OAuth2-style flow or a long-lived access token:
{"type": "auth", "access_token": "ABCDEFGH..."}
On success the server answers:
{"type": "auth_ok", "apexos_version": "2026.7.1"}
On failure it answers and then closes the connection:
{"type": "auth_invalid", "message": "Invalid access token or password"}
A malformed auth frame also produces auth_invalid (with a description of the schema error) followed by disconnect. Failed attempts count toward the HTTP login-ban machinery. If the refresh token behind the connection's access token is revoked later, the server cancels the WebSocket.
Auth frames may not contain both api_password and access_token (the two are mutually exclusive in the schema); token auth is the supported mechanism.
2. Command phase
After auth_ok, the client sends command frames. Every command frame must contain:
id— a positive integer that must strictly increase for each message on the connection (reuse yields anid_reuseerror)type— the command name
The server keeps the socket alive with protocol-level heartbeats (55 s interval); clients can additionally use the application-level ping command.
Frame types
Result — one per command, echoing the command id:
{"id": 12, "type": "result", "success": true, "result": {}}
On failure:
{
"id": 12,
"type": "result",
"success": false,
"error": {"code": "not_found", "message": "Service light.flash not found."}
}
Error payloads may also carry translation_key, translation_domain, and translation_placeholders for localizable errors. Defined error codes include: id_reuse, invalid_format, not_allowed, not_found, not_supported, apexos_error, service_validation_error, unknown_command, unknown_error, unauthorized, timeout, template_error.
Event — pushed for active subscriptions, tagged with the id of the subscribing command:
{"id": 5, "type": "event", "event": {"event_type": "state_changed", "data": {}}}
Pong — reply to ping:
{"id": 30, "type": "pong"}
Core commands
subscribe_events
{"id": 5, "type": "subscribe_events", "event_type": "state_changed"}
event_type is optional; omitting it subscribes to all events. Non-admin users may only subscribe to event types on the subscription allowlist; anything else raises unauthorized. For state_changed, forwarded events are filtered by the user's entity read permissions. The server confirms with a result frame, then pushes event frames tagged with the same id.
unsubscribe_events
{"id": 6, "type": "unsubscribe_events", "subscription": 5}
subscription is the id of the original subscribe command. Unknown subscriptions return a not_found error.
call_service
{
"id": 7,
"type": "call_service",
"domain": "light",
"service": "turn_on",
"service_data": {"brightness": 120},
"target": {"entity_id": "light.study"},
"return_response": false
}
Blocks until the service finishes. The result contains the call context; when return_response is true, it also contains the service response. Unknown services return not_found; validation failures return invalid_format or service_validation_error.
get_states
{"id": 8, "type": "get_states"}
Result is the array of all states the user is permitted to read.
subscribe_entities
Subscribes to a compressed state-diff stream (used by the frontend for efficiency). Events use short keys: a (added entities with compressed state), c (changed entities as + additions / - removals diffs), r (removed entity IDs).
get_config / get_services
get_config returns the core configuration; get_services returns all service descriptions (cached and delivered as a raw JSON fragment for speed).
ping
{"id": 9, "type": "ping"}
Answered with a pong frame carrying the same id.
render_template
{"id": 10, "type": "render_template", "template": "{{ states('sensor.kitchen_temperature') }}"}
Optional fields: entity_ids, variables, timeout, strict (default false), report_errors (default false). Sends a result frame, then pushes event frames with the rendered result and its listeners whenever a referenced entity changes.
Other built-in commands
fire_event, execute_script (admin), extract_from_target, entity/source, subscribe_bootstrap_integrations, subscribe_trigger (admin), test_condition (admin), subscribe_condition (admin), trigger_platforms/subscribe, condition_platforms/subscribe, get_triggers_for_target, get_conditions_for_target, get_services_for_target, validate_config, manifest/list, manifest/get, integration/setup_info, integration/descriptions, integration/wait, and supported_features.
supported_features lets a client declare optional protocol features; the only defined feature is coalesce_messages, which allows the server to batch multiple frames into a single WebSocket message.
Components register additional commands under their own prefixes — for example the auth commands (auth/current_user, auth/long_lived_access_token, …), conversation/process, and mobile_app/push_notification_channel.
Command registration model
Commands are plain handler functions registered into a per-instance registry:
- A handler is tagged with
@websocket_command(schema), where the schema dict must include the literaltypestring. The schema is merged with the base command schema ({"id": positive int}) and stored on the function. @async_responsewraps coroutine handlers so they run as background tasks;@require_adminand@ws_require_user(...)guard access (owner-only, active-user-only, system-user rules, etc.).websocket_api.async_register_command(apex, handler)stores the handler and schema in atype → (handler, schema)map. The built-in commands are registered at setup; any component may call the same function to add its own.- On each incoming frame the connection validates the minimal shape (
id,type), enforces increasing IDs, looks up the handler bytype(unknown types returnunknown_command), validates against the registered schema, and invokes the handler. Handlers reply viaconnection.send_result,connection.send_error, or push messages withconnection.send_message, and store unsubscribe callbacks inconnection.subscriptionskeyed by the commandid.