Zones
A zone is a physical space in your organization—a building, floor, wing, or room. Zones nest into a tree (Building A › Floor 2 › Lobby) and hold devices and entities from any integration. For a task walkthrough, see Organizing by zones. Zones can also offer Wi-Fi networks to guests on invitations.
Ownership
Section titled “Ownership”- The org owns the zone.
zone.org_idis the only source of truth for permissions, quota, and listing. No zone belongs to an integration. - One zone spans integrations. A “Kitchen” zone can hold a Shelly relay, a Home Assistant light, and a Tasmota plug at the same time.
- Deleting or transferring an integration never deletes a zone. Deleting an integration removes its provider links
only. Transferring an integration to another org leaves zones in the source org and clears zone references on the
transferred devices and entities; the transfer audit event records
detached_zone_count. A door also stops listing openers, lights, and cameras that end up in a different org than the door; the transfer preview names these underaffected.unwired_door_refsand asks for confirmation.
Org scope comes from the X-Org header, like other org resources. Listing covers the org subtree of X-Org.
Hierarchy
Section titled “Hierarchy”parent_zone_id points at the parent zone, or is null for a root zone. Validation on create and update:
| Rule | Result |
|---|---|
| Parent is missing or soft-deleted | 422 |
| Parent is in another org | 422 |
| New parent would form a cycle (the zone itself or one of its descendants) | 422 |
Provider links
Section titled “Provider links”A zone imported from a provider (a Home Assistant area, a Shelly room) records where it came from in
provider_links: a list of { integration_id, external_id }. Links are provenance, not ownership.
- One zone can carry links from several providers. The HA area “Kitchen” and the Shelly room “Kitchen” can both link to the same zone.
- Each
(integration_id, external_id)pair links to at most one zone. - Links are created by zone sync, not by
POST /zonesorPUT /zones/{id}.
Zone sync
Section titled “Zone sync”POST /integrations/{id}/zone-sync pulls the provider’s zones and placements. It’s idempotent:
- A provider zone that’s already linked updates in place, so a rename upstream doesn’t create a duplicate.
- An unlinked provider zone links to an existing org zone with the same normalized name only when exactly one zone matches. Otherwise sync creates a new zone.
- Device and entity placements from the provider are applied, except placements someone changed in OpenApp since
the last sync—those are kept and counted in
skipped_manual.
Response: { zones_created, zones_linked, devices_placed, entities_placed, skipped_manual }. Providers that don’t
expose zones return 422 unsupported.
Devices, entities, and the effective zone
Section titled “Devices, entities, and the effective zone”- A device has an optional
zone_id. - An entity has a three-way placement,
zone:
zone | Meaning |
|---|---|
{ "mode": "inherit" } | Use the device’s zone (may be none). Default for new entities. |
{ "mode": "explicit", "zone_id": "…" } | Use this zone, whatever the device’s zone is. |
{ "mode": "none" } | In no zone, even if the device has one. |
Entity responses add read-only effective_zone_id and zone_source ("entity", "device", "door", or null).
Every zone reference—device zone_id and explicit entity zone_id—must be a non-deleted zone in the same org,
otherwise 422. Inheritance is one level only: entity → device. Devices don’t inherit a zone from their integration.
Doors own their zone
Section titled “Doors own their zone”A door is a device of kind virtual_access_portal. Its zone_id is where it is, and the openers and lights it
lists (virtual_access.openers, virtual_access.lights) take that zone: their zone_source is "door" and
zone_door_id names the door. The door’s zone applies to public opens, invitations, zone-scoped policies, and
Wi-Fi, whatever the door is wired to. A door with no zone is in no zone.
Three writes return 409 with error_code: "door_zone_conflict":
- setting an explicit zone on an entity a door lists (
inheritandnoneare accepted); - wiring an entity that has an explicit zone into a door;
- wiring an entity into a door, or moving a door, so that two doors listing the same entity are in different zones. A door with no zone never conflicts. The message names the other doors.
Zone sync leaves door-listed entities alone and counts them in skipped_manual.
GET /integrations/{id}/access-zones returns a virtual_access site’s zones from its doors:
{ zones?, doors_without_zone, wifi_networks? }. Each zone lists its doors, and each network lists the site zones it
covers, with via_zone_id when it reaches a zone through an ancestor. zones and wifi_networks need zones:read
and are omitted without it; credentials are never returned. GET /integrations/{id}/access-overview adds zones
and wifi counts under the same rule.
Details: Entities and Devices.
Endpoints
Section titled “Endpoints”| Route | Contract |
|---|---|
POST /zones | Body { name, parent_zone_id? }. The org comes from X-Org. |
GET /zones | Query parent_zone_id, root=true (root zones only), q, limit, offset, include_deleted, only_deleted. |
GET /zones/tree | { items: ZoneTreeNode[] }; each node is { zone, children, device_count, entity_count }. Counts are direct (not including sub-zones) and use effective zones. |
GET /zones/{id} | The zone plus provider_links: [{ integration_id, external_id }]. |
GET /zones/{id}/summary | { zone, path, children, device_count, entity_count, descendant_entity_count }. path is [{ id, name }], root first, including the zone itself. |
PUT /zones/{id} | Body { name?, parent_zone_id? }. Send parent_zone_id: null to make it a root zone. Validated like create. |
DELETE /zones/{id} | Soft delete. See Deleting a zone. |
DELETE /zones/{id}/purge | Hard delete. |
POST /zones/{id}/merge | Body { source_zone_ids: [...] }. See Merging zones. |
GET /zones/{id}/location-binding, PUT | Same shape as the device location binding endpoints. See Location. |
POST /integrations/{id}/zone-sync | See Zone sync. |
GET /integrations/{id}/access-zones | See Doors own their zone. |
GET /integrations/{id}/zones doesn’t exist. List org zones with GET /zones or GET /zones/tree instead.
Deleting a zone
Section titled “Deleting a zone”DELETE /zones/{id} returns 409 zone_in_use while any of these still reference the zone:
- non-deleted child zones
- devices with this
zone_id - entities explicitly placed in it (
mode: "explicit")
Entities that only inherit the zone through their device don’t block the delete on their own—their device does.
Add ?cascade=detach to clear the references and delete in one step: child zones become root zones, devices get
zone_id: null, and explicit entities become mode: "none". The audit event records what was detached in
details.detached. Restoring a zone later doesn’t reattach anything.
Merging zones
Section titled “Merging zones”POST /zones/{id}/merge folds duplicate zones (for example two “Kitchen” rows) into the target zone {id}. It moves
devices, entities, child zones, and provider links from every source zone to the target, then soft-deletes the
sources. All zones must be in the same org. Needs both zones:update and zones:delete, and is audited as
zone.merged.
Zones with the same name are never merged automatically—a matching name isn’t proof they’re the same place.
Location
Section titled “Location”Zones are location owners with owner_kind=zone, using the same binding modes
as devices: inherit, explicit, and none.
- An
inheritzone takes its parent zone’s location, or the org’s location for a root zone. - An
explicitroot zone acts as a site—for example, set the address of “Building B” once and every room under it inherits it. - Devices don’t inherit a location through their zone. A device’s location still comes from its own binding, then its integration and org. One inheritance chain per owner keeps resolution unambiguous.
Filtering by zone
Section titled “Filtering by zone”| Route | Query | Matches |
|---|---|---|
GET /entities | zone_id, include_descendants=true | The entity’s effective zone, optionally including sub-zones. Combines with q and sort. |
GET /devices | zone_id | The device’s own zone. |
GET /audit/events and audit export | zone_id | Events recorded in that zone or any of its sub-zones. |
A malformed zone_id returns 400.
Every entity and device event—state changes, opens, holds, policy denials, and create/update/delete—records where the resource was when the event happened:
details.zone_id: the effective zone at that momentdetails.zone_path: ancestor zone ids, root first, includingzone_id
Both fields are absent when there was no zone. Zone events (zone.created, zone.updated, zone.deleted,
zone.merged, zone.purged) carry their own zone_path. Moving an entity later doesn’t rewrite past events.
Events from before zone snapshots shipped have no zone and don’t match a zone_id filter. See
Audit events.
Policies
Section titled “Policies”Policy types that act on outputs accept config.zone_id on org and integration scope. These are the same types
that accept output on a device policy in the policy catalog. The policy then applies
only when the acting entity’s effective zone is that zone or one of its sub-zones. Entities with zone.mode: "none"
are never in a zone, so zone-scoped policies don’t bind them.
Permissions
Section titled “Permissions”zones:create, zones:read, zones:update, zones:delete, and zones:list, checked against zone.org_id.
See Roles. Zones count against the org’s zones quota.
See Resource hierarchy for how zones fit the data model.