Skip to content
OpenAppPhysical access, fully managed
Login

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.

  • The org owns the zone. zone.org_id is 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 under affected.unwired_door_refs and asks for confirmation.

Org scope comes from the X-Org header, like other org resources. Listing covers the org subtree of X-Org.

parent_zone_id points at the parent zone, or is null for a root zone. Validation on create and update:

RuleResult
Parent is missing or soft-deleted422
Parent is in another org422
New parent would form a cycle (the zone itself or one of its descendants)422

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 /zones or PUT /zones/{id}.

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.

  • A device has an optional zone_id.
  • An entity has a three-way placement, zone:
zoneMeaning
{ "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.

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 (inherit and none are 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.

RouteContract
POST /zonesBody { name, parent_zone_id? }. The org comes from X-Org.
GET /zonesQuery 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}/purgeHard delete.
POST /zones/{id}/mergeBody { source_zone_ids: [...] }. See Merging zones.
GET /zones/{id}/location-binding, PUTSame shape as the device location binding endpoints. See Location.
POST /integrations/{id}/zone-syncSee Zone sync.
GET /integrations/{id}/access-zonesSee Doors own their zone.

GET /integrations/{id}/zones doesn’t exist. List org zones with GET /zones or GET /zones/tree instead.

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.

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.

Zones are location owners with owner_kind=zone, using the same binding modes as devices: inherit, explicit, and none.

  • An inherit zone takes its parent zone’s location, or the org’s location for a root zone.
  • An explicit root 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.
RouteQueryMatches
GET /entitieszone_id, include_descendants=trueThe entity’s effective zone, optionally including sub-zones. Combines with q and sort.
GET /deviceszone_idThe device’s own zone.
GET /audit/events and audit exportzone_idEvents 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 moment
  • details.zone_path: ancestor zone ids, root first, including zone_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.

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.

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.