Skip to content
OAOpenAppPhysical Security as a Service
Login

Resources

The SDK exposes the full OpenApp API surface through per-tag sub-clients. Each is available as an attribute on both Client and AsyncClient.

AttributeOpenAPI tagTypical methods
client.api_keysAPI Keyslist, create, delete
client.usersUserslist, get, create, update, delete, invite, upload_image, upload_image_from_url
client.orgsOrgslist, get, create, update, delete, upload_image, upload_image_from_url
client.devicesDeviceslist, get, update, delete, commands, upload_image, upload_image_from_url
client.entitiesEntitieslist, get, by_id, action, open, close, on, off, upload_image, upload_image_from_url
client.integrationsIntegrationslist, get, create, update, delete, upload_image, upload_image_from_url
client.zonesZoneslist, get, create, update, delete
client.lan_agentLAN agentstatus, agents
client.scriptingScriptingexecute
client.apartment_residentsApartment Residentslist, create, update, delete
client.public_accessPublic Accesslist, create, update, delete
client.authAuthwhoami
client.meMeget, update
client.eulaEULAcurrent, accept
client.statusStatusget

The authoritative list of methods per sub-client is the API reference (same openapi.json the SDK is generated from).

The current SDK exposes request and response payloads as plain Python dictionaries (dict[str, Any]). This keeps the surface stable while the typed Pydantic models are still under active generation. If you want type safety today, see Typing & Pydantic.

page = client.users.list(limit=100)
while page["items"]:
for user in page["items"]:
print(user["email"])
if not page.get("next_cursor"):
break
page = client.users.list(limit=100, cursor=page["next_cursor"])
entity = client.entities.by_id("01J00000000000000000000000")
result = entity.open(reason="visitor buzzed in", duration_s=5)
print(result["state"])
# Generic fallback for any action id:
custom = entity.action("switchable.open", reason="visitor buzzed in", duration_s=5)
print(custom["state"])

Upload org, user, integration, device, or entity images

Section titled “Upload org, user, integration, device, or entity images”

The API exposes the same multipart file upload for organizations, users, integrations, devices, and entities: POST /{resource}/{id}/image with image/jpeg, image/png, or image/webp (matching the dashboard).

from pathlib import Path
logo = Path("logo.png").read_bytes()
client.orgs.upload_image("01HORG...", data=logo, content_type="image/png", filename="logo.png")
avatar = Path("avatar.jpg").read_bytes()
client.users.upload_image("01HUSR...", data=avatar, content_type="image/jpeg", filename="avatar.jpg")
icon = Path("integration.jpg").read_bytes()
client.integrations.upload_image(
"01HZZZ...", data=icon, content_type="image/jpeg", filename="integration.jpg"
)
door_jpeg = Path("door.jpg").read_bytes()
client.devices.upload_image(
"01HXXX...",
data=door_jpeg,
content_type="image/jpeg",
filename="door.jpg",
)
face_png = Path("face.png").read_bytes()
client.entities.upload_image(
"01HYYY...",
data=face_png,
content_type="image/png",
filename="face.png",
)

When the image is already on the web, use upload_image_from_url (HTTP or HTTPS only):

client.users.upload_image_from_url("01HUSR...", url="https://example.com/avatar.jpg")
client.devices.upload_image_from_url("01HXXX...", url="https://example.com/door.jpg")

Use the client.scripting sub-client when you need OpenApp Scripting beyond individual REST calls:

result = client.scripting.execute(
script='entity_action("01J00000000000000000000000", "switchable.open", #{});'
)
print(result)

You can also keep the script in a .openapp file and pass its filename:

result = client.scripting.execute_file("open-door.openapp")
print(result)
device = client.devices.get("dev_abc")
for cmd in client.devices.commands(device_id=device["id"], limit=5)["items"]:
print(cmd["issued_at"], cmd["command"], cmd["result"])
  • Path parameters are accepted as positional or keyword arguments, matching the method signature (client.orgs.get("org_123")).
  • Query strings are keyword arguments; None values are omitted.
  • List query params are passed as Python lists and serialized with ?k=a&k=b.

Use interceptors if you need to add custom headers, log requests, or modify request/response bodies across all calls.