Entities
Entities are logical resources (apartments, doors, switches, cameras, …) attached to devices and driven by integrations. Most routes require X-Org and often output_options / pagination exactly as in OpenAPI — see Organization context & pagination. Shapes follow EntityResponse, CreateEntityRequest, UpdateEntityRequest, PatchEntityRequest, and PaginatedResponse — see the API reference.
Related: Devices (hardware rows), Zones (optional zone_id filter on GET /entities). Listing entities under an integration uses GET /integrations/{id}/entities — that route is tagged Integrations (documented when the Integrations chapter lands).
Operations vs wire routes
Section titled “Operations vs wire routes”| Concern | HTTP | operationId | Notes |
|---|---|---|---|
| List (org scope) | GET /entities | list_entities | X-Org; required output_options + pagination query objects; optional zone_id. |
| Create | POST /entities | create_entity | Body CreateEntityRequest. |
| Get / replace / patch / soft-delete | /entities/{id} | get_entity, update_entity, patch_entity, delete_entity | PatchEntityRequest shallow-merges entity_metadata. |
| Purge / restore | DELETE /entities/{id}/purge, POST /entities/{id}/restore | hard_delete_entity, restore_entity | |
| Execute action | POST /entities/{id}/actions/{action_id} | execute_entity_action | JSON body per integration (often {}). 501 if the provider does not implement the action. |
| Metadata schema (entity) | GET /entities/{id}/metadata-definition | get_entity_metadata_definition | Provider JSON Forms schema for editing entity_metadata. |
| List for device | GET /devices/{device_id}/entities | list_device_entities | X-Org + required pagination (+ filters per bundle). |
| Metadata schema (device) | GET /devices/{device_id}/entities/metadata-definition | get_device_entity_metadata_definition | Optional entity_type query (Python helper). |
| Apartment floors | GET /devices/{device_id}/apartment-floors | list_device_apartment_floors | Distinct floor list for apartment UIs. |
Multipart POST /entities/{id}/image may exist in some deployments; the Python SDK exposes upload_image / upload_image_from_url when wired — see Python — Resources.
SDK coverage
Section titled “SDK coverage”| Capability | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
| CRUD, patch, purge, restore, actions, metadata helpers, device listings | client.entities — includes by_id() fluent handle (open, close, on, off, action(...)) | client.entities() — thin JSON methods matching paths above | EntitiesAPIService (generated) | Not on façade |
GET /entities: Python list(...) forwards query keys from kwargs; Rust list() issues a bare GET /entities without required output_options / pagination — use transport() + RequestSpec for OpenAPI parity (same pattern as Devices). Go ListEntities enforces XOrg, OutputOptions, Pagination.
Typical errors
Section titled “Typical errors”401 / 403 for org or capability.404 for unknown entity or device.400 / 422 on validation.501 on execute_entity_action when unsupported. See Errors & retries.
Examples
Section titled “Examples”Execute an action (open)
Section titled “Execute an action (open)”await client.entities.by_id(entity_id).open()Equivalent: await client.entities.actions(entity_id, "open").
httpResp, err := client.EntitiesAPI.ExecuteEntityAction(ctx, entityID, "open"). Body(map[string]interface{}{}). Execute()if err != nil { return err}defer httpResp.Body.Close()use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let _ = client .entities() .invoke_action(entity_id, "open", &json!({})) .await?;const res = await fetch( `https://api.openapp.house/api/v1/entities/${entityId}/actions/open`, { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({}), },);
if (res.status === 501) { // Provider does not implement this action — surface a UI fallback. return;}const result = await res.json();The body is provider-specific and often {} for plain open/close. 501 is expected when the integration does not expose the action — branch on it instead of treating it as a generic transport error. Replace with AsyncClient.executeEntityAction once the Node façade exposes it.
List entities attached to a device
Section titled “List entities attached to a device”list_device_entities (GET /devices/{device_id}/entities) requires X-Org and pagination on the wire. Go’s builder enforces both; the Python and Rust facade helpers issue a bare GET without them, so for OpenAPI parity drop to _request / transport when your gateway rejects the unadorned path.
rows = await client.entities.by_device(device_id)For full wire parity with X-Org + pagination (or to filter by entity_type if the bundle accepts it):
page = await client._request( "GET", f"/devices/{device_id}/entities", headers={"X-Org": org_id}, query={"limit": 50, "offset": 0, "entity_type": "door"},)import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
resp, httpResp, err := client.EntitiesAPI.ListDeviceEntities(ctx, deviceID). XOrg(orgID). Pagination(openapiclient.PaginationQuery{ Limit: openapiclient.PtrInt32(50), Offset: openapiclient.PtrInt32(0), }). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = respTune PaginationQuery (limit, offset) to match your paging policy; some bundles also accept an EntityType(...) filter on the same builder.
use openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let rows = client.entities().by_device(device_id).await?;For X-Org + pagination + filters, drop to transport() + RequestSpec:
use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;use serde_json::Value;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let org_id = "01HORG00000000000000000000".to_string();let path = format!("/devices/{device_id}/entities");let page: Value = client .transport() .request_json(RequestSpec { method: Method::GET, path: &path, extra_headers: &[("X-Org", org_id)], query: &[ ("limit", Some("50".to_string())), ("offset", Some("0".to_string())), ("entity_type", Some("door".to_string())), ], ..Default::default() }) .await?;const url = new URL(`https://api.openapp.house/api/v1/devices/${deviceId}/entities`);url.searchParams.set("limit", "50");url.searchParams.set("offset", "0");const page = await fetch(url, { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId, },}).then((r) => r.json());Replace the fetch call with AsyncClient.listDeviceEntities once the Node façade exposes it.
Create entity (POST /entities)
Section titled “Create entity (POST /entities)”Body CreateEntityRequest — device_id and entity_type are required; optional name, zone_id, channel_index, external_id, and metadata (provider-specific). X-Org is required.
created = await client.entities.create( device_id=device_id, entity_type="door", name="Lobby gate",)import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
req := openapiclient.NewCreateEntityRequest(deviceID, "door")req.SetName("Lobby gate")created, httpResp, err := client.EntitiesAPI.CreateEntity(context.Background()). XOrg(orgID). CreateEntityRequest(*req). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = createduse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let body = json!({ "device_id": device_id, "entity_type": "door", "name": "Lobby gate",});let created = client.entities().create(&body).await?;const created = await fetch("https://api.openapp.house/api/v1/entities", { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({ device_id: deviceId, entity_type: "door", name: "Lobby gate", }),}).then((r) => r.json() as Promise<{ id: string; entity_type: string }>);Body matches CreateEntityRequest — device_id and entity_type are required; everything else (name, zone_id, channel_index, metadata) is optional and provider-specific. Replace with AsyncClient.createEntity once the Node façade exposes it.
Get / replace / patch (GET|PUT|PATCH /entities/{id})
Section titled “Get / replace / patch (GET|PUT|PATCH /entities/{id})”update_entity replaces the row with UpdateEntityRequest; patch_entity shallow-merges keys into entity_metadata via PatchEntityRequest (null clears a key). Both require X-Org.
row = await client.entities.get(entity_id)replaced = await client.entities.update(entity_id, name="Lobby gate v2")patched = await client.entities.patch(entity_id, metadata={"floor_number": 3})row, httpResp, err := client.EntitiesAPI.GetEntity(ctx, entityID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = row
upd := openapiclient.NewUpdateEntityRequest()upd.SetName("Lobby gate v2")replaced, httpResp2, err := client.EntitiesAPI.UpdateEntity(ctx, entityID). XOrg(orgID). UpdateEntityRequest(*upd). Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = replaced
p := openapiclient.NewPatchEntityRequest(map[string]interface{}{ "floor_number": 3,})patched, httpResp3, err := client.EntitiesAPI.PatchEntity(ctx, entityID). XOrg(orgID). PatchEntityRequest(*p). Execute()if err != nil { return err}defer httpResp3.Body.Close()_ = patcheduse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let row = client.entities().get(entity_id).await?;let replaced = client .entities() .update(entity_id, &json!({ "name": "Lobby gate v2" })) .await?;let patched = client .entities() .patch(entity_id, &json!({ "metadata": { "floor_number": 3 } })) .await?;const entityUrl = `https://api.openapp.house/api/v1/entities/${entityId}`;const baseHeaders = { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId,};const jsonHeaders = { ...baseHeaders, "content-type": "application/json" };
const row = await fetch(entityUrl, { headers: baseHeaders }).then((r) => r.json(),);
const replaced = await fetch(entityUrl, { method: "PUT", headers: jsonHeaders, body: JSON.stringify({ name: "Lobby gate v2" }),}).then((r) => r.json());
const patched = await fetch(entityUrl, { method: "PATCH", headers: jsonHeaders, body: JSON.stringify({ entity_metadata: { floor_number: 3 } }),}).then((r) => r.json());PUT replaces the row using UpdateEntityRequest; PATCH shallow-merges keys into entity_metadata via PatchEntityRequest — pass null for a key to clear it. Both require X-Org. Replace each fetch with AsyncClient helpers once the Node façade exposes them.
Soft delete, restore, hard delete (DELETE/POST lifecycle)
Section titled “Soft delete, restore, hard delete (DELETE/POST lifecycle)”delete_entity is reversible (soft delete); restore_entity undoes it; hard_delete_entity purges the row irrecoverably. All require X-Org.
await client.entities.delete(entity_id)restored = await client.entities.restore(entity_id)await client.entities.purge(entity_id)_, httpResp, err := client.EntitiesAPI.DeleteEntity(ctx, entityID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp.Body.Close()
restored, httpResp2, err := client.EntitiesAPI.RestoreEntity(ctx, entityID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = restored
_, httpResp3, err := client.EntitiesAPI.HardDeleteEntity(ctx, entityID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp3.Body.Close()use openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
client.entities().delete(entity_id).await?;let restored = client.entities().restore(entity_id).await?;client.entities().purge(entity_id).await?;const headers = { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId,};
await fetch(`https://api.openapp.house/api/v1/entities/${entityId}`, { method: "DELETE", headers,});
const restored = await fetch( `https://api.openapp.house/api/v1/entities/${entityId}/restore`, { method: "POST", headers },).then((r) => r.json());
await fetch(`https://api.openapp.house/api/v1/entities/${entityId}/purge`, { method: "DELETE", headers,});Soft delete is reversible via /restore until /purge runs. All three require X-Org; /purge removes the row irrecoverably. Replace each fetch with AsyncClient helpers once the Node façade exposes them.
Metadata definitions and apartment floors
Section titled “Metadata definitions and apartment floors”get_entity_metadata_definition returns the JsonForms-style schema dashboards use to edit entity_metadata. get_device_entity_metadata_definition is the device-scoped variant (optional entity_type filter). list_device_apartment_floors is a small helper for apartment UIs that need a distinct floor list.
schema = await client.entities.metadata_definition(entity_id)device_schema = await client.entities.device_entities_metadata_definition( device_id, entity_type="apartment")floors = await client.entities.apartment_floors(device_id)schema, httpResp, err := client.EntitiesAPI.GetEntityMetadataDefinition(ctx, entityID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = schema
q := openapiclient.NewDeviceEntityMetadataDefinitionQuery()q.SetEntityType("apartment")deviceSchema, httpResp2, err := client.EntitiesAPI.GetDeviceEntityMetadataDefinition(ctx, deviceID). XOrg(orgID). DeviceEntityMetadataDefinitionQuery(*q). Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = deviceSchema
floors, httpResp3, err := client.EntitiesAPI.ListDeviceApartmentFloors(ctx, deviceID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp3.Body.Close()_ = floorsuse openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let schema = client.entities().metadata_definition(entity_id).await?;let device_schema = client .entities() .device_entities_metadata_definition(device_id) .await?;let floors = client.entities().apartment_floors(device_id).await?;The Rust helper sends a bare GET with no entity_type query — for parity with get_device_entity_metadata_definition in OpenAPI:
use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;use serde_json::Value;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let org_id = "01HORG00000000000000000000".to_string();let path = format!("/devices/{device_id}/entities/metadata-definition");let device_schema: Value = client .transport() .request_json(RequestSpec { method: Method::GET, path: &path, extra_headers: &[("X-Org", org_id)], query: &[("entity_type", Some("apartment".to_string()))], ..Default::default() }) .await?;const schema = await fetch( `https://api.openapp.house/api/v1/entities/${entityId}/metadata-definition`, { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId } },).then((r) => r.json());
const url = new URL( `https://api.openapp.house/api/v1/devices/${deviceId}/entities/metadata-definition`,);url.searchParams.set("entity_type", "apartment");const deviceSchema = await fetch(url, { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId },}).then((r) => r.json());
const floors = await fetch( `https://api.openapp.house/api/v1/devices/${deviceId}/apartment-floors`, { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId } },).then((r) => r.json());Replace the fetch calls with AsyncClient helpers once the Node façade exposes them.