---
title: Bridge protocol
description: The RPCs the bridge serves on its Unix socket, and their wire format.
---

The bridge serves [Effect](https://effect.website) RPCs as newline-delimited JSON on a Unix socket. From TypeScript, use [`@timmo001/effect-ha-bridge`](/libraries), which handles the framing for you. This page is for other languages and for debugging.

The socket is at the [socket path](/configuration#socket-path), in a directory only you can open (`0700`), and is itself only accessible by you (`0600`).

## RPCs

| RPC | Payload | Result |
| --- | --- | --- |
| `GetEntities` | `{ "target": Target, "domain"?: string }` | A list of entity updates, one for each entity the target matches |
| `WatchEntities` | `{ "target": Target, "domain"?: string }` | A stream of entity updates: the current state of each matching entity, then every change |
| `CallAction` | `{ "action": string, "data"?: object, "target"?: Target, "return_response"?: boolean }` | The action's response when `return_response` is `true`, otherwise `null` |
| `GetConfig` | `null` | Home Assistant's config, such as `time_zone`, `location_name` and `version` |
| `CameraSnapshot` | `{ "target": Target }` | `{ "contentType": string, "data": string }`, with the image bytes base64-encoded in `data` |
| `Search` | `{ "query"?: string, "target"?: Target, "kinds"?: ("entity" \| "device" \| "area")[], "domain"?: string, "deviceClass"?: string, "limit"?: number, "offset"?: number }` | `{ "results": SearchMatch[], "total": number, "unavailable": string[] }` |
| `RenderTemplate` | `{ "template": string, "variables"?: object, "strict"?: boolean, "timeout"?: number }` | `{ "result": Json, "warnings": string[] }`, the first render |
| `WatchTemplate` | `{ "template": string, "variables"?: object, "strict"?: boolean, "timeout"?: number }` | A stream of renders, each `{ "result": Json }` or `{ "error": string, "level": string }`, rendering again after reconnects |
| `WatchEvents` | `{ "event_type"?: string }` | A stream of events, each with `event_type`, `data`, `origin`, `time_fired` and `context` |
| `FireEvent` | `{ "event_type": string, "event_data"?: object }` | Nothing |
| `GetHistory` | `{ "target": Target, "domain"?: string, "start_time": string, "end_time"?: string, "no_attributes"?: boolean, "all_changes"?: boolean }` | An object keyed by entity ID, each a list of `{ "state", "attributes"?, "last_changed", "last_updated" }` |
| `GetLogbook` | `{ "target"?: Target, "domain"?: string, "start_time": string, "end_time"?: string }` | A list of logbook entries, each with an ISO `when` |
| `WatchLogbook` | `{ "target"?: Target, "domain"?: string }` | A stream of new logbook entries, carrying on from each reconnect |
| `WatchTrigger` | `{ "trigger": object \| object[], "variables"?: object }` | A stream of `{ "variables": object, "context": object \| null }`, one each time a trigger fires |
| `TestCondition` | `{ "condition": object, "variables"?: object }` | `{ "result": boolean, "template_errors"?: string[] }` |
| `WatchCondition` | `{ "condition": object }` | A stream of `{ "result": boolean }` or `{ "error": string }`, each with optional `template_errors`, sent when the result changes |
| `GetSystemLog` | `{ "search"?: string }` | Core's logged errors and warnings, newest first, each as `system_log/list` sends it: `name`, `message`, `level`, `source`, `timestamp`, `first_occurred`, `exception` and `count` |
| `WatchSystemLog` | `{ "search"?: string }` | A stream of new errors and warnings, and repeats of earlier ones, read every 5 seconds |
| `GetLog` | `{ "source": LogSource, "lines"?: number, "boot"?: number, "latest"?: boolean, "verbose"?: boolean, "integration"?: string, "search"?: string }` | A list of the log's latest records, oldest first |
| `WatchLog` | `{ "source": LogSource, "lines"?: number, "verbose"?: boolean, "integration"?: string, "search"?: string }` | A stream of the log's latest records, then each new one, following reconnects |
| `GetLogBoots` | `null` | The host's boots, an object of boot IDs keyed by offset, `"0"` for the current boot |
| `GetLogApps` | `null` | A list of installed apps, each `{ "slug", "name", "state", "version" }` |
| `SearchLogs` | `{ "query": string, "sources"?: ("errors" \| LogSource)[], "lines"?: number, "limit"?: number, "offset"?: number }` | `{ "results": { "source", "record", "score" }[], "total": number }`, best match first |
| `GetEntityConfig` | `{ "domain": "automation" \| "script", "entity_id": string }` | `{ "domain", "entity_id", "id": string \| null, "editable": boolean, "config": object }` |
| `CreateEntityConfig` | `{ "domain": "automation" \| "script", "id"?: string, "config": object }` | `{ "domain", "id" }` once Home Assistant has validated and saved it |
| `UpdateEntityConfig` | `{ "domain": "automation" \| "script", "entity_id": string, "config": object }` | `{ "domain", "id" }` once Home Assistant has validated and saved it |
| `DeleteEntityConfig` | `{ "domain": "automation" \| "script", "entity_id": string }` | `{ "domain", "id" }` once it's removed |
| `ValidateEntityConfig` | `{ "domain": "automation" \| "script", "config": object }` | `{ "valid": boolean, "errors": { "part", "message" }[], "unknown": { "kind", "id" }[] }`, without saving anything |

A target is `{ "entity_id"?, "device_id"?, "area_id"?, "floor_id"?, "label_id"? }`, each a string or a list of strings, as in Home Assistant. Each value is an ID or a name, which the bridge resolves to an ID: an exact ID first, then an entity ID without the domain when there is a `domain` (or the action's domain), then a unique case-insensitive name. Home Assistant then expands the target, so an area includes entities on devices in it. `domain` keeps only entities in that domain.

An entity update is `{ "state": EntityState, "name": string }`. `state` is the state object Home Assistant sends, with `entity_id`, `state`, `attributes`, `last_changed`, `last_reported`, `last_updated` and `context` (`id`, `parent_id`, `user_id`); everything past `state` can be missing. `name` is the display name described in [Bar JSON](/using/bar-json#output). `WatchEntities` expands the target again after Home Assistant reconnects and when a registry changes, sending the state of every entity it newly matches, and sends an entity's state again when its display name changes.

A search match is `{ "kind", "id", "name", "score", "matched" }` plus `domain`, `deviceClass`, `device`, `parentDevice`, `area` and `floor` when they're known. `total` counts every match before `offset` and `limit` (default 20) are applied, and `unavailable` lists any registries the bridge couldn't load, such as `area_registry`. Without a query, `Search` lists every item the target and filters allow, by kind then name, each with a score of 100. A target keeps only the entities, devices and areas Home Assistant says it refers to. `Search` reads the bridge's cache, asking Home Assistant only to expand a target, and fails with `SearchEmpty` (`{ "_tag": "SearchEmpty" }`) when it has no query, target, `domain` or `deviceClass`. [Searching](/using/search) describes the matching.

`GetEntities`, `CallAction`, `GetConfig`, `CameraSnapshot` and `Search` with a target fail with `HomeAssistantError` (`{ "_tag": "HomeAssistantError", "message": string }`) when Home Assistant rejects the request or the bridge isn't connected to it. Every RPC with a target fails with `TargetError` (`{ "_tag": "TargetError", "message": string }`) when a name matches nothing or more than one item, when Home Assistant doesn't know an ID, or when `CameraSnapshot`'s target doesn't match exactly one camera. `WatchEntities` waits for a connection instead of failing. `CallAction` takes the same `action`, `data` and `target` keys as actions in Home Assistant automations.

A log source is one of `"core"`, `"supervisor"`, `"host"`, `"audio"`, `"cli"`, `"dns"`, `"multicast"` and `"observer"`, or `{ "app": string }` with an app's slug. `boot` is an offset, `0` for this boot and `-1` for the one before. `latest` and `integration` only apply to Core's log; `integration` reads Core's live log, which needs Home Assistant 2026.11 or newer, as does `WatchLog` for Core. A search is fuzzy, as `Search` is: `GetLog`, `WatchLog` and the system log RPCs keep matching records in time order, and `SearchLogs` ranks them, reading Core's errors and Core's log when `sources` is left out. [Logs](/using/logs) describes each log.

The log RPCs fail with `LogUnavailable` (`{ "_tag": "LogUnavailable", "reason": "admin" | "supervisor" | "permission" | "unsupported", "message": string }`) before asking Home Assistant when the bridge's token doesn't belong to an admin, when the log needs a Supervisor that Home Assistant doesn't have, or when Home Assistant can't serve the request, such as Core's live log on an older Core. Inside a Home Assistant app, they fail with `permission` when the Supervisor refuses the app because its `config.yaml` lacks the access the log needs. Clients can skip the log on `LogUnavailable` rather than treat it as a failure. They fail with `HomeAssistantError` when Home Assistant rejects the request or the bridge isn't connected to it.

The config RPCs work as Home Assistant's automation and script editors do. `GetEntityConfig` returns the config any automation or script was loaded from, wherever it's defined. `id` is the editor's key, an automation's `id` or a script's key in `scripts.yaml`, and `editable` says whether it's in `automations.yaml` or `scripts.yaml`, the only files the editor and these RPCs write. `CreateEntityConfig` adds one, making up an automation's `id` or a script's key from its `alias` when `id` is left out. `UpdateEntityConfig` replaces all of one's config. Home Assistant validates a config before saving it, then reloads it, so a new entity appears shortly after. They fail with `ConfigUnavailable` (`{ "_tag": "ConfigUnavailable", "reason": "admin" | "not_editable", "message": string }`) before asking Home Assistant when the bridge's token doesn't belong to an admin, or when an update or delete targets one that isn't in the editor's file. They fail with `HomeAssistantError` when Home Assistant rejects the config, with its reason, or doesn't know the entity.

`ValidateEntityConfig` checks a config without saving it, and doesn't need an admin token. Home Assistant checks its triggers, conditions and actions (a script's `sequence`) as the automation editor does, and each part it rejects, or a required part that's missing, is in `errors` as `{ "part": "triggers" | "conditions" | "actions", "message" }`. `unknown` lists what the config names that Home Assistant doesn't have, each `{ "kind": "entity" | "action" | "device" | "area" | "floor" | "label", "id" }`, much as Spook reports them. It reads the IDs the config gives outright, such as `entity_id`, `area_id` and `action`. It skips templates, steps with `enabled: false`, event data and variables, and entities in domains an action can create as it runs (`group`, `device_tracker` and `persistent_notification`). A registered entity that's disabled isn't unknown. `valid` is true when both lists are empty.

## Messages

Each line is one JSON message. Send a request:

```json
{"_tag":"Request","id":"1","tag":"GetEntities","payload":{"target":{"entity_id":"sun.sun"}},"headers":[]}
```

A single result comes back as an `Exit` with the same ID:

```json
{"_tag":"Exit","requestId":"1","exit":{"_tag":"Success","value":[{"state":{"entity_id":"sun.sun","state":"above_horizon","attributes":{}},"name":"Sun"}]}}
```

A failure has `"_tag":"Failure"` and a `cause` list instead of `value`. An RPC's own error is a `Fail` entry:

```json
{"_tag":"Exit","requestId":"2","exit":{"_tag":"Failure","cause":[{"_tag":"Fail","error":{"_tag":"HomeAssistantError","message":"camera snapshot: StatusCode: non 2xx status code (404 GET ...)"}}]}}
```

A malformed request comes back as a `Die` entry, such as `{"_tag":"Die","defect":"Expected null"}` when `GetConfig` has no `payload`.

Keep the connection open until the reply arrives. When the client closes its side, the bridge ends that client's requests that are still running.

Streams send `Chunk` messages, each with one or more values:

```json
{"_tag":"Chunk","requestId":"1","values":[{"state":{"entity_id":"sun.sun","state":"above_horizon","attributes":{}},"name":"Sun"}]}
```

After each chunk, the bridge waits for an `Ack` before sending the next one:

```json
{"_tag":"Ack","requestId":"1"}
```

To stop a stream, send `{"_tag":"Interrupt","requestId":"1"}` or close the connection.

## Try it

```bash
socket="$XDG_RUNTIME_DIR/ha-bridge/ha-bridge.sock"

{
  echo '{"_tag":"Request","id":"1","tag":"GetConfig","payload":null,"headers":[]}'
  sleep 1
} | socat - "UNIX-CONNECT:$socket"
```
