Admin API
The panel is a client of this API; everything it does, you can do over HTTP. These are the admin routes under /api/atm/admin/ and the request and response bodies they expect.
Home Assistant session, admin only
Every admin route requires a valid Home Assistant session and the admin role. An ATM token can never authenticate here, not even a pass-through token. See Security.
Conventions
- Bodies
- JSON,
Content-Type: application/json. Bodies over 1 MB are rejected with 413. - Request ID
- Every response carries
X-ATM-Request-ID, matching the audit log entry. - Errors
- Error responses are
{ "error": code, "message": text }, with an optionalsuggestionsarray. Codes:unauthorized(401),forbidden(403),not_found(404),invalid_request(400),request_too_large(413).
Tokens
GET /api/atm/admin/tokens List tokens
POST /api/atm/admin/tokens Create a token
GET /api/atm/admin/tokens/{id} Get one token
PATCH /api/atm/admin/tokens/{id} Update a token
DELETE /api/atm/admin/tokens/{id} Revoke a token
POST /api/atm/admin/tokens/{id}/rotate Generate a new raw value
POST /api/atm/admin/tokens/{id}/presets Save current settings as a preset
PATCH /api/atm/admin/tokens/{id}/presets/{pid} Rename a preset
DELETE /api/atm/admin/tokens/{id}/presets/{pid} Delete a preset (active preset refused)
POST /api/atm/admin/tokens/{id}/presets/{pid}/apply Make the token match a preset
GET /api/atm/admin/tokens/{id}/stats Request counters
GET /api/atm/admin/tokens/{id}/connection last_used_at + request_count
GET /api/atm/admin/tokens/{id}/audit Audit log for one token
GET /api/atm/admin/tokens/archived List archived tokens
DELETE /api/atm/admin/tokens/archived/{id} Delete an archived record
Create (POST /tokens) takes a name and optional lifetime and limits. The response is the full token record plus a one-time token field (the raw value, never returned again).
{
"name": "claude-code", // required, 3-32 chars: letters, digits, _ or -
"expires_at": "2026-07-01T00:00:00Z", // optional ISO-8601, omit for no expiry
"pass_through": false, // optional
"confirm_pass_through": false, // required to be true when pass_through is true
"rate_limit_requests": 60, // optional, 0 disables rate limiting
"rate_limit_burst": 10 // optional
}
Presets (all four endpoints return 403 while the token_presets_enabled setting is off): a preset is a named snapshot of the token's full configuration. Create takes {"name": "..."} and snapshots the CURRENT settings (at most 8 per token, names unique per token). Apply makes the token match the stored preset; applying a preset other than the active one first auto-saves the live state into the outgoing active preset, while applying the active preset reverts to its saved state. Applying a preset that would enable pass-through on a scoped token requires {"confirm_pass_through": true}. All return the updated token record.
Update (PATCH /tokens/{id}) accepts any subset of these fields. Capability fields each take "deny", "allow", or "confirm". Enabling pass-through requires confirm_pass_through: true in the same request, or the call returns 400. A token can also be renamed by sending name (same format and uniqueness rules as creation, excluding the token's own name); expires_at stays immutable. Renaming rebuilds the token's per-token sensor entities under the new name.
{
"persona": "voice_assistant",
"pass_through": false,
"confirm_pass_through": false,
"confirm_inline_wait_seconds": 60,
"rate_limit_requests": 60,
"rate_limit_burst": 10,
"cap_config_read": "allow",
"cap_template_render": "allow",
"cap_log_read": "deny",
"cap_search": "allow",
"cap_registry_read": "allow",
"cap_traces": "deny",
"cap_diagnostics": "deny",
"cap_broadcast": "deny",
"cap_service_response": "allow",
"cap_automation_write": "deny",
"cap_script_write": "deny",
"cap_scene_write": "deny",
"cap_helper_write": "deny",
"cap_physical_control": "confirm",
"cap_restart": "deny",
"cap_integration_write": "deny",
"cap_lovelace_write": "deny",
"cap_registry_write": "deny",
"cap_radio_write": "deny",
"cap_backup": "deny",
"cap_filesystem": "deny",
"cap_yaml_edit": "deny",
"use_assist_exposure": false,
"announce_all_tools": false
}
Setting a persona applies that persona's full preset across every capability in one request; see Personas. use_assist_exposure and announce_all_tools are advanced options described in Operations. confirm_inline_wait_seconds is how long a confirm-gated call holds the response open waiting for an admin decision before falling back to an immediate pending_approval reply; it must be 0 (off, admin-API-only) or an integer from 30 to 180, and defaults new tokens to 60.
Permissions
GET /api/atm/admin/tokens/{id}/permissions Read the tree
PUT /api/atm/admin/tokens/{id}/permissions Replace the tree
PATCH /api/atm/admin/tokens/{id}/permissions/domains/{node} Set one domain node
PATCH /api/atm/admin/tokens/{id}/permissions/devices/{node} Set one device node
PATCH /api/atm/admin/tokens/{id}/permissions/entities/{node} Set one entity node
GET /api/atm/admin/tokens/{id}/resolve/{entity} Explain effective permission
GET /api/atm/admin/tokens/{id}/scope Readable / writable lists
A node PATCH sets a single node's state, with an optional per-token hint (max 200 characters; null clears it). The PUT replaces the whole tree; nodes use the same shape.
{
"state": "GREEN", // GREY | YELLOW | GREEN | RED
"hint": "Rachel's desk lamp, not the ceiling light" // optional, <= 200 chars, null clears
}
resolve returns the decision and the ancestor that made it: { entity_id, resolution_path: [{level, state}], effective, effective_hint }.
Entity hints
Global hints apply to every token that can see the entity. A per-token permission-node hint (above) always takes precedence over the global one. Both are capped at 200 characters.
GET /api/atm/admin/entity-hints The global entity_id > hint map
PUT /api/atm/admin/entity-hints/{entity_id} Set or clear one global hint
{ "hint": "Garage side door" } // null clears the hint
Entities
GET /api/atm/admin/entities The entity tree (domains, devices, entities)
GET /api/atm/admin/entities?force_reload=1 Rebuild the cached tree
Approvals
GET /api/atm/admin/approvals List approvals (filter by status, token_id)
GET /api/atm/admin/approvals/{id} Get one approval
POST /api/atm/admin/approvals/{id}/approve Approve and run the held action
POST /api/atm/admin/approvals/{id}/reject Reject with an optional reason
DELETE /api/atm/admin/approvals/{id} Cancel a pending approval
List accepts ?status=pending|approved|rejected|expired|cancelled, ?token_id=, ?limit=, and ?offset=, and returns { approvals: [...], total, limit, offset }. Approve and reject take small optional bodies.
// POST .../approve
{ "note": "looks fine" } // optional
// POST .../reject
{ "reason": "not this one" } // optional
Configuration history
GET /api/atm/admin/versions Recent changes across all resources
GET /api/atm/admin/versions?resource_type=&resource_id= One resource's version history
GET /api/atm/admin/versions/{id} One version, with full before/after
POST /api/atm/admin/versions/{id}/restore Re-apply a version (admin authority)
Every agent-made create, edit, or delete of an automation, script, scene, helper, dashboard layout, the raw configuration.yaml, or a scoped file (www/, themes/, custom_templates/) is snapshotted here. With no query parameters the list returns the most recent versions across all resources (the panel's Changes feed); with both resource_type and resource_id it returns that resource's history, newest first. List rows are compact summaries; fetch a single version for the full before / after config. Restore re-applies one of the version's two snapshots (an optional side body field selects before or after; it defaults to after, falling back to before for a delete), editing the resource if it still exists or recreating it if it was deleted (in place under its original id for automations, scripts, and scenes, so re-restoring is idempotent; helpers get a new HA-assigned id), and records the change as a rollback attributed to you. Configs are stored raw so a restore can round-trip; the data is admin-only and no more sensitive than the YAML it mirrors. A file or configuration.yaml snapshot over 100 KB is kept as a metadata-only marker, so restoring that version returns an error instead.
MESA profiles
Profiles exist at the entity, domain, integration, and area levels, plus a canonical-tag vocabulary and a validation view. The profile document shape is defined by mesa-core; see MESA for how it fits into ATM.
GET /api/atm/admin/mesa/profiles List entity profiles (domain, tag, area, origin, cursor)
GET /api/atm/admin/mesa/profiles/{entity_id} Stored + effective profile for one entity
PUT /api/atm/admin/mesa/profiles/{entity_id} Create or replace an entity profile
DELETE /api/atm/admin/mesa/profiles/{entity_id} Remove an entity profile
GET /api/atm/admin/mesa/domains List domain profiles
GET/PUT/DELETE /api/atm/admin/mesa/domains/{domain} One domain profile
GET /api/atm/admin/mesa/integrations List integration profiles
GET/PUT/DELETE /api/atm/admin/mesa/integrations/{integration} One integration profile
GET /api/atm/admin/mesa/integration-options Integrations with entities (picker source)
GET /api/atm/admin/mesa/areas List area profiles
GET/PUT/DELETE /api/atm/admin/mesa/areas/{area_id} One area profile
GET/PUT /api/atm/admin/mesa/defaults Deployment-default profile (the fallback level)
GET /api/atm/admin/mesa/vocabulary Canonical tags and roots
GET /api/atm/admin/mesa/issues Validation issues + orphans (?refresh=1)
POST /api/atm/admin/mesa/orphans/clear Delete every orphaned entity, area, and integration profile in one call
GET /api/atm/admin/mesa/export Export every profile as a portable archive
POST /api/atm/admin/mesa/import Import a portable archive
POST /api/atm/admin/mesa/suggestions/dismiss Dismiss one profile suggestion
POST /api/atm/admin/mesa/suggestions/restore Restore a dismissed suggestion, or all of them
The orphans/clear action recomputes the orphan lists against the live registries, then deletes exactly those profiles (the per-profile DELETE endpoints remain for one-off removals). It returns the deleted ids grouped by kind and a total count. Profiles are never deleted automatically; this is an explicit admin action.
{
"semantic_profile": { }, // mesa-core schema; see the mesa-core repo
"privacy_classification": { }
}
Export / import move profiles between deployments using mesa-core's own portable archive format. Export takes no body and returns every stored profile (entity, domain, integration, and area levels, plus the deployment default) verbatim, with no filtering, so the response is exactly what import expects back. Import validates every document in the archive and never writes an invalid one; a document that fails validation is reported in invalid rather than aborting the rest of the archive.
{
"archive": { }, // a document previously returned by GET .../export
"on_conflict": "skip" // skip (default, keep the existing profile) | overwrite
}
Import responds with { imported, overwritten, skipped_existing, invalid }, each a list or map keyed by profile.
Suggestions (surfaced as the suggestions array on GET .../issues) are computed, never auto-applied; dismiss and restore only change whether a suggestion is shown, they never touch a profile. Dismiss requires the key to match a suggestion in the currently computed set. Restore takes either key for one suggestion or {"all": true} to clear every dismissal; both return the current suggestions and dismissed_suggestions lists alongside the action's own result field (dismissed or restored).
// POST .../suggestions/dismiss
{ "key": "naked_risky:lock" }
// POST .../suggestions/restore
{ "key": "naked_risky:lock" } // restore one
{ "all": true } // or clear every dismissal
Agent Chat
Agent Chat is the in-panel LLM chat that runs an agentic loop against a chosen token. Provider accounts ("instances", since more than one account of the same kind is allowed, for example two Claude keys) are configured through these admin routes and stored in a dedicated secrets file separate from tokens; the chat turn itself streams over Server-Sent Events rather than returning a single JSON body. Every route here, including the chat route, requires the same Home Assistant admin session as the rest of this page.
GET /api/atm/admin/agentcli/providers List provider accounts
POST /api/atm/admin/agentcli/providers Add a provider account (validated before it is stored)
DELETE /api/atm/admin/agentcli/providers/{instance_id} Remove a provider account
GET /api/atm/admin/agentcli/providers/{instance_id}/models List that account's available models
POST /api/atm/admin/agentcli/probe Validate credentials and list models, without storing anything
POST /api/atm/agentcli/chat Run one streaming agent turn (Server-Sent Events)
A provider account holds a kind (one of claude, deepseek, chatgpt, gemini, grok, minimax, openrouter, ollama, ollama_cloud) plus either an api_key or, for the keyless local ollama kind, a base_url. Create validates the credential against the provider before saving it; list never returns the key.
{
"kind": "claude",
"api_key": "sk-ant-...", // required for every kind except ollama
"base_url": "http://localhost:11434", // required for ollama; ignored otherwise
"model": "claude-opus-4-8" // optional; a provider with no default is chosen later
}
The response is { "instance": { id, kind, name, model, base_url } }, the same shape GET .../providers returns as { "instances": [...] }. name is a display label the server derives (the provider kind, with a discriminator like the last four characters of the key appended only when more than one account of that kind exists). Probe accepts the same body as create but stores nothing, so a Settings form can validate a credential and populate a model dropdown before committing; it responds { "ok": true, "models": [...] } or { "ok": false, "error": "...", "models": [] } instead of an HTTP error, since a failed probe is an expected outcome, not a server fault.
Chat takes the token to act as, the provider account to use, the new user message, and the prior turns (the browser holds the transcript and resends it each turn; this endpoint keeps no conversation state of its own).
{
"token_id": "...", // required, the ATM token this turn acts as
"instance_id": "...", // required, a stored provider account id
"model": "claude-opus-4-8", // optional, overrides the account's saved model
"user": "turn off the kitchen lights", // required, the new user message
"messages": [ ], // optional prior turns: [{"role": "user"|"assistant", "content": "..."}]
"options": { "thinking": true, "effort": "high", "show_thinking": false, "temperature": 0.7, "max_tokens": 8192 }
}
The response is text/event-stream, not a single JSON body: named SSE frames (ready, text_delta, thinking_delta, tool_use_complete, message_done, error) carry the turn's progress as it streams, with periodic unnamed keepalive comments so an idle stretch (the model thinking, an approval awaiting a decision) does not get dropped by an intermediary. Every tool call the agent makes runs through the same capability gates, MESA checks, and audit log as a real MCP client on that token; a confirm-gated call surfaces as an inline approval card in the panel rather than a plain pending_approval reply.
The chat route is kill-switch-gated; provider config is not
The four provider config/model routes above are kill-switch-immune, like the rest of the admin API. POST /api/atm/agentcli/chat is agent activity, not administration, so it is gated by the kill switch the same way the MCP and REST proxy endpoints are: with the kill switch on, the route is not even registered.
Settings and system
GET /api/atm/admin/settings Read global settings
PATCH /api/atm/admin/settings Update global settings (any subset)
GET /api/atm/admin/audit Global audit log (limit, offset, token_id, outcome, ip)
GET /api/atm/admin/info Version info
POST /api/atm/admin/voice_agent/pipeline Create an Assist pipeline pointed at ATM's voice agent
DELETE /api/atm/admin/voice_agent/pipeline Remove the ATM-created pipeline
GET /api/atm/admin/ai_task/preferred Read the "Data generation tasks" default vs ATM's entity
POST /api/atm/admin/ai_task/preferred Make ATM the default data-gen entity
DELETE /api/atm/admin/ai_task/preferred Clear ATM as the default
DELETE /api/atm/admin/wipe Wipe tokens, audit log, and settings
Settings are described in Operations. A PATCH may include any subset of them. The voice_agent/pipeline and ai_task/preferred routes back the one-click Assist and AI Task setup buttons in the panel: POST a pipeline requires { "preferred": true|false } and needs the voice agent fully configured; the AI Task routes report and set Home Assistant's single default data-generation entity.
{
"kill_switch": false,
"disable_all_logging": false,
"log_allowed": true,
"log_denied": true,
"log_rate_limited": true,
"log_entity_names": true,
"log_client_ip": true,
"notify_on_rate_limit": false,
"notify_on_approval": true,
"audit_flush_interval": 15, // minutes: 0 (never), 5, 10, 15, 30, 60
"audit_log_maxlen": 10000,
"mesa_mode": "advisory", // off | advisory | enforced
"mesa_inject_enabled": false, // experimental in-context MESA buttons; admin-only, off by default
"token_presets_enabled": false, // token settings presets; enabling seeds a default preset on every token
"agentcli_global": true, // Agent Chat floats over all of Home Assistant, not just the ATM panel (default on)
"agentcli_scrollback_lines": 100, // Agent Chat memory: 0-5000 lines, shown in the panel as "Chat memory"
"assist_bound_token_id": null, // Assist Tool Provider: token whose tools "ATM (scoped)" exposes (null = unbound)
"voice_agent_enabled": false, // Voice Agent: ATM as an HA conversation agent, on its own token/provider/model
"voice_agent_token_id": null,
"voice_agent_provider_id": null, // an Agent Chat provider account id
"voice_agent_model": null,
"ai_task_enabled": false, // AI Task: ATM as an HA AI Task entity, on its own token/provider/model
"ai_task_token_id": null,
"ai_task_provider_id": null, // an Agent Chat provider account id
"ai_task_model": null
}
The wipe route is intentionally guarded: it requires an exact confirmation string. It is scoped by three independent flags, so you choose what to delete. wipe_core (default true) clears every token and archived token, all settings back to their defaults, global entity hints, the audit log, and the configuration/version change history, plus in-memory request-tracking state (the rate limiter's windows, per-token request counters, and rate-limit/stale-tools notification markers) and any queued pending approvals (dismissing their notifications). wipe_providers (default true) deletes the Agent Chat provider accounts and their stored API keys from .storage/atm_agentcli_secrets. wipe_mesa (default false) deletes all MESA profiles from .storage/atm_mesa; it is off by default so a reset keeps hand-authored safety policy unless you ask otherwise. Omitting a flag uses its default (a bare { "confirm": "WIPE" } clears core data and provider keys but leaves MESA intact). The panel's Data Management card exposes all three as switches.
{
"confirm": "WIPE",
"wipe_core": true, // tokens, settings, audit, versions, pending approvals (default true)
"wipe_providers": true, // Agent Chat provider accounts + API keys (default true)
"wipe_mesa": false // MESA safety profiles (default false)
}
PATCH is read-modify-write under a lock
Token and record PATCH handlers load, modify, and save atomically, so two admins editing the same token at once cannot clobber each other.
The token-scoped proxy routes (states, services, history, and so on) are listed in Operations.