Zones
Zones are org-owned physical spaces (buildings, floors, rooms) that hold devices and entities from any integration. See the Zones reference for validation, delete, merge, and placement rules. All routes require X-Org — see Organization context & pagination. Response bodies follow ZoneResponse; mutations use CreateZoneRequest / UpdateZoneRequest (both use LocalizedString for display names) — see the API reference.
Zones are listed per org with GET /zones or GET /zones/tree. POST /zones takes the org from X-Org; there’s no integration_id in the body.
Operations vs wire routes
Section titled “Operations vs wire routes”| Concern | HTTP | operationId | Notes |
|---|---|---|---|
| List (org scope) | GET /zones | list_zones | X-Org required; optional parent_zone_id, root=true, q, limit, offset, include_deleted, only_deleted. |
| Tree | GET /zones/tree | get_zone_tree | { items: ZoneTreeNode[] } with direct device_count / entity_count. |
| Create | POST /zones | create_zone | Body CreateZoneRequest: name (LocalizedString), optional parent_zone_id. 422 for a missing, deleted, foreign-org, or cyclic parent. |
| Get | GET /zones/{id} | get_zone | Includes provider_links. |
| Summary | GET /zones/{id}/summary | get_zone_summary | path, children, and counts for a room view. |
| Update | PUT /zones/{id} | update_zone | Body UpdateZoneRequest: optional name, parent_zone_id (null makes it a root). |
| Merge | POST /zones/{id}/merge | merge_zones | Body { source_zone_ids }. Needs zones:update and zones:delete. |
| Soft delete | DELETE /zones/{id} | delete_zone | 409 zone_in_use while referenced; ?cascade=detach clears references. |
| Hard delete | DELETE /zones/{id}/purge | hard_delete_zone |
SDK coverage
Section titled “SDK coverage”| Capability | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
| Lifecycle + org listing | client.zones — list, tree, get_by_name, create, get, update, delete, purge | client.zones() — same | ZonesAPI | Not on façade |
Thin wrappers send JSON bodies matching OpenAPI; extend transport + RequestSpec when you need full query/header parity on every route variant.
Typical errors
Section titled “Typical errors”401 / 403 when the caller can’t access the org. 404 for unknown zone ids. 422 for an invalid parent or a zone in another org. 409 zone_in_use on delete without cascade=detach. 400 for a malformed zone_id filter. Errors follow ApiErrorResponse — see Errors & retries.
Examples
Section titled “Examples”List zones in an org
Section titled “List zones in an org”zones = await client.zones.list("org_123")tree = await client.zones.tree()zones, httpResp, err := client.ZonesAPI.ListZones(ctx). XOrg("org_123"). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = zonesuse openapp_sdk::Client;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let zones = client.zones().list("org_123").await?;let tree = client.zones().tree().await?;Add X-Org via transport extra_headers or interceptors when required by your deployment.
const url = new URL("https://openapp.house/api/v1/zones");url.searchParams.set("root", "true");
const zones = await fetch(url, { headers: { "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", "x-org": orgId, },}).then((r) => r.json() as Promise<Array<{ id: string }>>);Use GET /zones/tree for the whole hierarchy in one call. Replace with AsyncClient.listZones once the Node façade exposes it.
Create a zone
Section titled “Create a zone”zone = await client.zones.create( name={"value": {"en": "Lobby"}}, parent_zone_id=floor_id,)name := *openapiclient.NewLocalizedString(map[string]string{"en": "Lobby"})req := openapiclient.NewCreateZoneRequest(name)req.SetParentZoneId(floorID)resp, httpResp, err := client.ZonesAPI.CreateZone(ctx). XOrg("org_123"). CreateZoneRequest(*req). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = respuse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let zone = client .zones() .create(&json!({ "name": { "value": { "en": "Lobby" } }, "parent_zone_id": floor_id })) .await?;const zone = await fetch("https://openapp.house/api/v1/zones", { method: "POST", headers: { "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({ name: { value: { en: "Lobby" } }, parent_zone_id: floorId, }),}).then((r) => r.json() as Promise<{ id: string; org_id: string }>);Body matches CreateZoneRequest — name is wrapped in LocalizedString ({ value: { en: "…" } }), and parent_zone_id is optional. Replace with AsyncClient.createZone once the Node façade exposes it.
Get a zone (GET /zones/{id})
Section titled “Get a zone (GET /zones/{id})”X-Org is required; optional include_deleted / only_deleted query flags surface soft-deleted rows for dashboards.
zone = await client.zones.get(zone_id)For deleted/metadata flags, send the request via AsyncClient._request with explicit query keys until the helper exposes them.
zone, httpResp, err := client.ZonesAPI.GetZone(ctx, zoneID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = zoneAdd .IncludeDeleted(true) / .IncludeMetadata(true) when you need wire parity with the OpenAPI query.
use openapp_sdk::Client;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let zone = client.zones().get(zone_id).await?;Use transport() + RequestSpec with X-Org and query keys when the bare path is rejected by your gateway.
const url = new URL(`https://openapp.house/api/v1/zones/${zoneId}`);url.searchParams.set("include_deleted", "false");
const zone = await fetch(url, { headers: { "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", "x-org": orgId, },}).then((r) => r.json());The optional include_deleted / only_deleted keys serialize as flat URLSearchParams entries (same shape as output_options in the rest of the SDK). Replace with AsyncClient.getZone once the Node façade exposes it.
Update a zone (PUT /zones/{id})
Section titled “Update a zone (PUT /zones/{id})”Body UpdateZoneRequest — all fields optional: name (LocalizedString), parent_zone_id (null makes it a root zone). A new parent is validated like create. Replaces the row (no merge).
updated = await client.zones.update( zone_id, name={"value": {"en": "Lobby (renamed)"}},)import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
name := openapiclient.NewLocalizedString(map[string]string{"en": "Lobby (renamed)"})upd := openapiclient.NewUpdateZoneRequest()upd.SetName(*name)updated, httpResp, err := client.ZonesAPI.UpdateZone(context.Background(), zoneID). XOrg(orgID). UpdateZoneRequest(*upd). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = updateduse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let updated = client .zones() .update( zone_id, &json!({ "name": { "value": { "en": "Lobby (renamed)" } } }), ) .await?;const updated = await fetch(`https://openapp.house/api/v1/zones/${zoneId}`, { method: "PUT", headers: { "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({ name: { value: { en: "Lobby (renamed)" } }, }),}).then((r) => r.json());PUT replaces the row — fields you omit are dropped, not merged. Body matches UpdateZoneRequest with the same LocalizedString wrapper as create. Replace with AsyncClient.updateZone once the Node façade exposes it.
Soft delete and hard delete (DELETE /zones/{id}, DELETE /zones/{id}/purge)
Section titled “Soft delete and hard delete (DELETE /zones/{id}, DELETE /zones/{id}/purge)”delete_zone is reversible at the data layer (the row stays under include_deleted=true); hard_delete_zone removes the row irrecoverably. Both require X-Org. Soft delete returns 409 zone_in_use while child zones, devices, or explicit entities reference the zone; add cascade=detach to clear them first (see Deleting a zone). There is no dedicated restore route in the published bundle — recover by re-querying with include_deleted=true and re-creating if needed.
await client.zones.delete(zone_id)await client.zones.purge(zone_id)_, httpResp, err := client.ZonesAPI.DeleteZone(ctx, zoneID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp.Body.Close()
_, httpResp2, err := client.ZonesAPI.HardDeleteZone(ctx, zoneID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp2.Body.Close()use openapp_sdk::Client;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
client.zones().delete(zone_id).await?;client.zones().purge(zone_id).await?;const headers = { "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", "x-org": orgId,};
await fetch(`https://openapp.house/api/v1/zones/${zoneId}`, { method: "DELETE", headers,});
await fetch(`https://openapp.house/api/v1/zones/${zoneId}/purge`, { method: "DELETE", headers,});Soft delete leaves the row visible behind include_deleted=true; /purge is irreversible. There is no dedicated zone restore route — re-list with include_deleted=true before recreating. Replace with AsyncClient.deleteZone / hardDeleteZone once the Node façade exposes them.