Manage site people and listing members via API
Shared apartment buildings need delegation: the building operator owns the intercom and common doors, while each listing (an apartment, office, or suite in the directory) manages its own members. OpenApp exposes two layers on a Virtual Access site:
| Layer | Routes | What it controls |
|---|---|---|
| Site people | /integrations/{id}/people[/{user_id}] | Who is on the site, with a site role: admin, resident, or technician |
| Listing members | /entities/{id}/directory-listing-members[/{member_id}] | Who belongs to one listing, as admin (manages the listing) or member, and who receives its calls |
This guide is for integrators and building admins automating resident lists — not for guests. See Model by sector (shared apartment building) and Directory listings.
Prerequisites
Section titled “Prerequisites”- Virtual Access integration id (
INTEGRATION_ID) - Organization id (
X-Org) and an API key withentities:updatein that org (required to add or remove site people) - Target user’s OpenApp
user_id
Full SDK reference: Site people. HTTP reference: the Directory Listing Members tag in the API reference.
List site people
Section titled “List site people”SDK
rows = await client.site_people.list(integration_id)const rows = await fetch( `${apiBase}/integrations/${integrationId}/people`, { headers: { "x-api-key": apiKey, "x-org": orgId, }, },).then((r) => r.json());use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;
let path = format!("/integrations/{integration_id}/people");let rows: Vec<serde_json::Value> = client .transport() .request_json(RequestSpec { method: Method::GET, path: &path, extra_headers: &[("X-Org", org_id.to_string())], ..Default::default() }) .await?;resp, err := client.DirectoryListingMembersAPI.ListPeople(ctx, integrationID). XOrg(orgID). Execute()if err != nil { return err}defer resp.Body.Close()Send X-Org if your client doesn’t inject it — see the Site people note on headers.
HTTP API (curl)
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 INTEGRATION_ID='01HINTEGRATION00000000000000'
curl -sS \ -H "X-API-Key: ${OPENAPP_API_KEY}" \ -H "X-Org: ${OPENAPP_ORG_ID}" \ "${OPENAPP_API_BASE}/integrations/${INTEGRATION_ID}/people"Add a person to the site
Section titled “Add a person to the site”SDK
await client.site_people.add( integration_id, user_id=user_id, role="resident", # admin | resident | technician)await fetch(`${apiBase}/integrations/${integrationId}/people`, { method: "POST", headers: { "x-api-key": apiKey, "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({ user_id: userId, role: "resident" }),});use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;use serde_json::json;
let path = format!("/integrations/{integration_id}/people");client .transport() .request_json::<serde_json::Value, _>(RequestSpec { method: Method::POST, path: &path, body: Some(&json!({ "user_id": user_id, "role": "resident" })), extra_headers: &[("X-Org", org_id.to_string())], ..Default::default() }) .await?;payload := *openapiclient.NewAddSitePersonPayload("resident", userID)resp, err := client.DirectoryListingMembersAPI.AddPerson(ctx, integrationID). XOrg(orgID). AddSitePersonPayload(payload). Execute()if err != nil { return err}defer resp.Body.Close()role is one of admin, resident, or technician; anything else returns 400. OpenApp grants the matching site role on the integration’s org.
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 '{ "user_id": "01HUSER00000000000000000000", "role": "resident" }' \ "${OPENAPP_API_BASE}/integrations/${INTEGRATION_ID}/people"To put that person in a listing, add them as a listing member: POST /entities/{listing_id}/directory-listing-members with user_id and role (admin or member). A listing admin (or anyone with entities:update in the org) manages that listing’s members — this is how each apartment manages its own residents without site-wide permissions.
Remove a person from the site
Section titled “Remove a person from the site”Removing a person drops their site roles and their listing assignments on that site.
SDK
await client.site_people.remove(integration_id, user_id)await fetch( `${apiBase}/integrations/${integrationId}/people/${userId}`, { method: "DELETE", headers: { "x-api-key": apiKey, "x-org": orgId, }, },);use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;
let path = format!("/integrations/{integration_id}/people/{user_id}");client .transport() .request_json::<(), ()>(RequestSpec { method: Method::DELETE, path: &path, extra_headers: &[("X-Org", org_id.to_string())], ..Default::default() }) .await?;resp, err := client.DirectoryListingMembersAPI.DeletePerson(ctx, integrationID, userID). XOrg(orgID). Execute()if err != nil { return err}defer resp.Body.Close()HTTP API (curl)
curl -sS -X DELETE \ -H "X-API-Key: ${OPENAPP_API_KEY}" \ -H "X-Org: ${OPENAPP_ORG_ID}" \ "${OPENAPP_API_BASE}/integrations/${INTEGRATION_ID}/people/${USER_ID}"Automation patterns
Section titled “Automation patterns”| Pattern | Approach |
|---|---|
| Apartment portal in your app | On “add roommate”, the listing admin calls POST /entities/{listing_id}/directory-listing-members |
| Sync from property DB | Nightly job: diff GET .../people against your source of truth |
| Offboarding | DELETE /integrations/{id}/people/{user_id} on lease end |
Every change is audited (site_person.*, directory_listing_member.*) — see Query access and audit activity. Pair with Virtual intercom flow so listing members appear in the lobby directory.