Native app integration
The official companion apps for ApexOS are ApexConnect for iOS and Android. They integrate with the core through the mobile_app component, which provides a registration endpoint, a per-device webhook channel, sensor entities, device tracking, and push notifications. Any native app can implement the same contract.
Registration
An app first obtains tokens via the auth flow, then registers the device:
POST /api/mobile_app/registrations
Authorization: Bearer <ACCESS_TOKEN>
Request fields:
| Field | Required | Notes |
|---|---|---|
app_id | yes | Application identifier |
app_name | yes | Human-readable app name |
app_version | yes | |
device_name | yes | Falls back to device_id if it slugifies to empty |
manufacturer | yes | |
model | yes | |
os_name | yes | |
os_version | no | |
device_id | no | |
supports_encryption | no (default false) | Request an encryption key |
app_data | no | Push configuration, see below |
Unknown extra fields are stripped, so newer apps can safely send more data. Response (201 Created):
{
"cloudhook_url": null,
"remote_ui_url": null,
"secret": "d5e...hex",
"webhook_id": "abcd1234..."
}
webhook_id— a randomly generated secret identifier; all subsequent app→core traffic is POSTed to/api/webhook/{webhook_id}without any Authorization header (the webhook ID is the credential; keep it secret and prefer HTTPS).secret— returned only whensupports_encryptionwas true: a hex key for NaClSecretBoxpayload encryption.cloudhook_url/remote_ui_url— populated only when an active cloud subscription is connected and the user is not local-only.
Registration triggers a config-entry flow that creates the device and its entities.
Webhook channel
Every message is a POST to /api/webhook/{webhook_id} with an envelope:
{"type": "<action>", "data": { }}
or, when encryption is enabled:
{"type": "<action>", "encrypted": true, "encrypted_data": "<base-encoded ciphertext>"}
Once a registration declares supports_encryption, plaintext webhooks are rejected with an encryption_required error. Responses to encrypted registrations are wrapped the same way ({"encrypted": true, "encrypted_data": ...}). A webhook for a deleted registration returns HTTP 410 Gone. Invalid payloads are deliberately answered with an empty-OK response so probes learn nothing.
Webhook actions
type | Payload | Result |
|---|---|---|
update_registration | app_version, device_name, manufacturer, model, optional os_version, app_data | Updated (sanitized) registration |
get_config | — | Instance config: latitude, longitude, elevation, apex_device_id, unit_system, location_name, time_zone, components, version, theme_color, entity enable/disable map, cloud URLs when available |
call_service | domain, service, optional service_data | Executes the service |
fire_event | event_type, optional event_data | Fires a bus event |
render_template | map of key → {template, variables} | Map of key → rendered result (per-key errors reported inline) |
update_location | optional location_name, gps (+ required gps_accuracy when gps present), battery, speed, altitude, course, vertical_accuracy, in_zones | Updates the device tracker |
register_sensor | see below | Creates/updates a sensor entity (201) |
update_sensor_states | list of {type, unique_id, state, attributes?, icon?} | Batch state updates; per-sensor errors (not_registered, invalid_format) |
enable_encryption | — | Returns a fresh secret; fails with encryption_already_enabled if already on |
stream_camera | camera_entity_id | MJPEG (and HLS when available) stream paths |
conversation_process | text, optional language, conversation_id | Runs the text through the conversation pipeline (see Intents) |
get_zones | — | All zone entities |
scan_tag | tag_id | Fires a tag-scanned event for this device |
Sensor registration
register_sensor fields:
type(required) —sensororbinary_sensorunique_id(required) — unique per device; the entity's stored unique ID is namespaced with the webhook IDname(required) — displayed as"<device name> <sensor name>"state—null, bool, int, float, or string (defaultnull)- optional:
attributes,device_class,unit_of_measurement,state_class,entity_category,icon(defaultmdi:cellphone),disabled
Re-registering an existing unique_id updates the entity registry entry (name, device class, unit, category, icon, disabled state) and applies the state — so registration is idempotent and also usable for reconfiguration.
Push notifications
Notifications flow core → app through a notify service created per registration. How they are delivered is controlled by the app_data object the app sends at registration (and can change later via update_registration):
{
"push_token": "<platform push token>",
"push_url": "https://<push relay>/api/notify",
"push_websocket_channel": true
}
push_tokenandpush_urlmust be sent together (push_cloudgroup). When set, the core POSTs notification payloads to the app-provided push URL, forwarding the push token — the app vendor operates that relay endpoint.- The relay's response may include rate-limit info under
rateLimits(successful,maximum,errors,resetsAt), which the core logs and enforces (arate_limit_exceeded_sending_notificationerror is raised when exceeded). push_websocket_channel: trueenables a local push channel over the WebSocket API instead: the app subscribes withmobile_app/push_notification_channel(optionallysupport_confirm: true) and acknowledges deliveries withmobile_app/push_notification_confirm.- iOS live activities are supported via
start_live_activity_tokeninapp_data.
A clear_notification message with a matching tag removes a previously delivered notification.
Companion-app WebSocket usage
Beyond the webhook channel, the apps use the regular WebSocket API with their long-lived session: subscribe_events, call_service, get_states, the auth commands, and the mobile_app/push_notification_channel subscription described above.