Build an agent that controls physical access
An access-control agent (or integration bot) calls OpenApp on behalf of an operator: list doors, open a gate when a parcel arrives, or revoke a guest invite at checkout. This guide covers the minimum wiring — credentials, org scope, and executing switchable.open safely.
For sector-specific workflows (private home, STR, hotel, campus, locker), pair this with Integrate OpenApp with your existing software and Model by sector.
Prerequisites
Section titled “Prerequisites”- An OpenApp account with permission to manage the target organization.
- An API key for server-to-server use.
- The organization id (ULID) and at least one entity id for a door or gate (from the dashboard or
GET /entities).
Store secrets in environment variables — never embed keys in prompts, llms.txt, or public repos.
export OPENAPP_API_BASE='https://openapp.house/api/v1'export OPENAPP_API_KEY='https://openapp.house_openapp_YOUR_SECRET'export OPENAPP_ORG_ID='01HORG00000000000000000000'export ENTITY_ID='01HENTITY000000000000000000'Organization context (X-Org)
Section titled “Organization context (X-Org)”Many routes are scoped to an organization. Pass the org id in the X-Org header when the API reference or Organization context docs say it is required (for example GET /devices, integration admin routes).
SDK
devices = await client.devices.list()import { AsyncClient } from "@tomers/openapp-sdk";
const client = new AsyncClient(apiKey, { org: orgId });const devices = await client.listDevices(orgId);use openapp_sdk::Client;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let devices = client.devices().list().await?;page, httpResp, err := client.DevicesAPI.ListDevices(ctx). XOrg(orgID). OutputOptions(*openapiclient.NewMultiResourceOutputOptionsQuery()). Pagination(openapiclient.PaginationQuery{ Limit: openapiclient.PtrInt32(50), Offset: openapiclient.PtrInt32(0), }). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = pagePass org context via client headers where required — see Organization context.
HTTP API (curl)
curl -sS "${OPENAPP_API_BASE}/devices" \ -H "X-API-Key: ${OPENAPP_API_KEY}" \ -H "X-Org: ${OPENAPP_ORG_ID}"Execute a door action
Section titled “Execute a door action”Doors and relays use entity actions. The common unlock action is switchable.open — the same call as Open the door with the HTTP API.
SDK
await client.entities.actions(entity_id, "switchable.open")const res = await fetch( `${apiBase}/entities/${entityId}/actions/switchable.open`, { method: "POST", headers: { "x-api-key": apiKey, "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({}), },);if (!res.ok) throw new Error(`open failed: ${res.status}`);use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
client .entities() .invoke_action(entity_id, "switchable.open", &json!({})) .await?;httpResp, err := client.EntitiesAPI.ExecuteEntityAction(ctx, entityID, "switchable.open"). Body(map[string]interface{}{}). Execute()if err != nil { return err}defer httpResp.Body.Close()Pass org context on list calls via headers where required — see Entities and Organization context.
HTTP API (curl)
curl -sS -X POST \ -H "X-API-Key: ${OPENAPP_API_KEY}" \ -H "Content-Type: application/json" \ -H "X-Org: ${OPENAPP_ORG_ID}" \ -d '{}' \ "${OPENAPP_API_BASE}/entities/${ENTITY_ID}/actions/switchable.open"Discover entities before acting
Section titled “Discover entities before acting”Agents should list or resolve entities instead of hard-coding ids when possible:
GET /integrations→ pick the Virtual Access or hardware integration.GET /integrations/{id}/entitiesorGET /entitieswith pagination.- Match
entity_typeand metadata (door name, zone, external id). - Call
POST /entities/{id}/actions/{action_id}only on the resolved id.
See Agent-relevant API index for operation groupings.
Safety checklist
Section titled “Safety checklist”| Risk | Mitigation |
|---|---|
| Wrong door opened | Resolve entity by id + human-readable label; confirm in UI for first deploy. |
| Stale or stolen API key | Rotate keys in the dashboard; use dedicated scoped keys or agent credentials. A scoped credential reaches only the routes and roles in its scope and gets 403 everywhere else. |
| Agent reaches the wrong doors | Set the agent’s allowed_entity_ids; it is intersected with the key’s entity list. An agent with no list is not entity-restricted. |
| Unattended unlock | Set the agent’s confirmation mode (always or physical_only) so opens need a preview token; document org policy. |
| Door not classified | switchable.open and switchable.toggle return 409 (life_safety_class_required or life_safety_attestation_required) until the opening is classified and attested — see Life safety and egress. |
| Audit trail | Every switchable.* action is recorded as entity.action.succeeded or entity.action.denied, attributed to the agent, API key, or user. Pull it with the audit API, export it, or stream it to your endpoint with HMAC-signed webhooks — see Query access and audit activity. |
OpenApp orchestrates access; life-safety and compliance remain the operator’s responsibility — see Access control architecture.
Next steps
Section titled “Next steps”- Time-bound guest invitations for STR and hotel flows.
- Virtual intercom flow for directory + call + unlock.
- SDK overview to replace raw HTTP in production code.