Public Access
Public Access routes drive visitor experiences: invite URLs, building portal pages, and short-lived sessions (door open, lights, call/video signaling). Authorization is carried by opaque ids in the path (inviteToken, publicPortalId, sessionId), not org X-Org / entity RBAC. Response shapes include PublicInviteResponse, PublicPortalResponse, PublicSessionResponse, etc. — see the API reference.
You still point the SDK at the same API base URL; the transport may attach your API key for routing, but these routes do not represent an authenticated org user. For admin-side portal configuration, see Integrations.
Operations vs wire routes (summary)
Section titled “Operations vs wire routes (summary)”| Area | HTTP | operationId | Notes |
|---|---|---|---|
| Invites | GET /public/access/invites/{inviteToken} | get_public_invite | Dashboard payload for an invite link. |
POST .../claim | post_public_invite_claim | Claim / associate the invite (body per bundle when required). | |
POST .../execute | post_public_invite_execute | Body PublicInviteExecuteRequest → PublicInviteExecuteResponse. | |
POST .../session | post_public_invite_session | Start a guest session from an invite. | |
| Portals | GET /public/access/portals/{publicPortalId} | get_public_portal | Portal config + mode. |
POST .../lights, POST .../open | post_public_portal_lights, post_public_portal_open | Control paths on the public portal id. | |
GET .../reachable | get_public_portal_reachable | Health / connectivity probe. | |
POST .../sessions | post_public_portal_sessions | Create session from portal (PublicPortalCreateSessionRequest). | |
GET .../targets | get_public_portal_targets | Callable targets for the portal UI. | |
| Sessions | GET /public/access/sessions/{sessionId} | get_public_session | Poll guest session state. |
POST .../cancel, POST .../decline | post_public_session_cancel, post_public_session_decline | End or reject. | |
POST .../lights, POST .../open, POST .../notify-message | post_public_session_lights, post_public_session_open, post_public_session_notify_message | In-session actions. | |
GET .../streams | get_public_session_streams | WebRTC / streaming metadata (PublicSessionStreamsResponse). |
Path segments in OpenAPI use inviteToken, publicPortalId, sessionId — SDK helpers take plain strings.
SDK coverage
Section titled “SDK coverage”| Surface | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
| Full table above | client.public_access | client.public_access() — methods mirror path names (get_invite, claim_invite, portal_open, session_streams, …) | PublicAccessAPIService | getPublicInvite, claimPublicInvite only — extend via transport / other SDKs for portals and sessions |
Typical errors
Section titled “Typical errors”400 on bad tokens or payloads.401 / 403 on claim/execute when policy blocks the action.404 for expired or unknown tokens.403 on post_public_invite_execute when forbidden. See Errors & retries.
Examples
Section titled “Examples”Load an invite and claim
Section titled “Load an invite and claim”invite = await client.public_access.get_invite(token)await client.public_access.claim_invite(token, **{})inv, httpResp, err := client.PublicAccessAPI.GetPublicInvite(ctx, token).Execute()if err != nil { return err}defer httpResp.Body.Close()_, httpResp2, err := client.PublicAccessAPI.PostPublicInviteClaim(ctx, token).Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = invuse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let invite = client.public_access().get_invite(token).await?;client .public_access() .claim_invite(token, &json!({})) .await?;import { AsyncClient } from "@tomers/openapp-sdk";
const client = new AsyncClient("https://api.openapp.house/api/v1_openapp_YOUR_SECRET");const invite = await client.getPublicInvite(token);await client.claimPublicInvite(token);The façade exposes invite GET + claim only; use Python / Go / Rust for execute, portal, and session routes.
Open from a public portal (session)
Section titled “Open from a public portal (session)”result = await client.public_access.portal_open(portal_id, reason="visitor")httpResp, err := client.PublicAccessAPI.PostPublicPortalOpen(ctx, portalID).Execute()if err != nil { return err}defer httpResp.Body.Close()The generated client issues POST with an empty body when the bundle defines no request schema for this operation.
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 result = client .public_access() .portal_open(portal_id, &json!({ "reason": "visitor" })) .await?;await fetch( `https://api.openapp.house/api/v1/public/access/portals/${portalId}/open`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reason: "visitor" }), },);The OpenAPI bundle defines no request schema, so the wire body is optional — pass { "reason": "…" } when your deployment surfaces it on audit logs (Python / Rust forward the same body). Replace with AsyncClient portal helpers once the Node façade exposes them.
Execute an invite (POST .../invites/{token}/execute)
Section titled “Execute an invite (POST .../invites/{token}/execute)”Body PublicInviteExecuteRequest is a single required field, grant_id (the grant published in get_public_invite’s payload). Response PublicInviteExecuteResponse includes ok, optional message, door_auto_close_duration, and lights_auto_off_duration.
out = await client.public_access.execute_invite(token, grant_id="grant_abc123")import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
body := *openapiclient.NewPublicInviteExecuteRequest("grant_abc123")resp, httpResp, err := client.PublicAccessAPI.PostPublicInviteExecute(context.Background(), token). PublicInviteExecuteRequest(body). 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://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let out = client .public_access() .execute_invite(token, &json!({ "grant_id": "grant_abc123" })) .await?;const out = await fetch( `https://api.openapp.house/api/v1/public/access/invites/${token}/execute`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ grant_id: "grant_abc123" }), },).then((r) => r.json() as Promise<{ ok: boolean; message?: string; door_auto_close_duration?: number; lights_auto_off_duration?: number;}>);grant_id comes from the get_public_invite payload — surface out.message to the visitor when out.ok is false, instead of swallowing it. Replace with AsyncClient.executeInvite once the Node façade exposes it.
Start a portal session (call / video)
Section titled “Start a portal session (call / video)”post_public_portal_sessions opens a guest WebRTC session on the portal. Body PublicPortalCreateSessionRequest requires target_entity_id and mode (e.g. "call", "video"). The response carries session_id, caller_token, expires_at, and PeerJS routing details.
session = await client.public_access.portal_start_session( portal_id, target_entity_id=entity_id, mode="call",)body := *openapiclient.NewPublicPortalCreateSessionRequest(entityID, "call")resp, httpResp, err := client.PublicAccessAPI.PostPublicPortalSessions(ctx, portalID). PublicPortalCreateSessionRequest(body). 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://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let session = client .public_access() .portal_start_session( portal_id, &json!({ "target_entity_id": entity_id, "mode": "call" }), ) .await?;const session = await fetch( `https://api.openapp.house/api/v1/public/access/portals/${portalId}/sessions`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ target_entity_id: entityId, mode: "call" }), },).then((r) => r.json() as Promise<{ session_id: string; caller_token: string; expires_at: string;}>);Persist session.caller_token — every later session_* call (lights / open / notify-message) accepts it as the ?token=… query param when the deployment requires per-session caller auth. Replace with AsyncClient portal session helpers once the Node façade exposes them.
Drive a guest session (poll / cancel / decline / open)
Section titled “Drive a guest session (poll / cancel / decline / open)”Once a session exists, get_public_session polls state, post_public_session_cancel ends it from the caller side, post_public_session_decline rejects from the callee side, and post_public_session_open triggers an entry-side action while the call is live.
state = await client.public_access.get_session(session_id)await client.public_access.session_open(session_id)await client.public_access.cancel_session(session_id)await client.public_access.decline_session(session_id)state, httpResp, err := client.PublicAccessAPI.GetPublicSession(ctx, sessionID).Execute()if err != nil { return err}defer httpResp.Body.Close()_ = state
_, httpResp2, err := client.PublicAccessAPI.PostPublicSessionOpen(ctx, sessionID).Execute()if err != nil { return err}defer httpResp2.Body.Close()
_, httpResp3, err := client.PublicAccessAPI.PostPublicSessionCancel(ctx, sessionID).Execute()if err != nil { return err}defer httpResp3.Body.Close()
_, httpResp4, err := client.PublicAccessAPI.PostPublicSessionDecline(ctx, sessionID).Execute()if err != nil { return err}defer httpResp4.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 state = client.public_access().get_session(session_id).await?;client.public_access().session_open(session_id, &json!({})).await?;client.public_access().cancel_session(session_id).await?;client.public_access().decline_session(session_id).await?;const apiBase = "https://api.openapp.house";const sessionUrl = `${apiBase}/public/access/sessions/${sessionId}`;
const state = await fetch(sessionUrl).then((r) => r.json());
await fetch(`${sessionUrl}/open`, { method: "POST" });await fetch(`${sessionUrl}/cancel`, { method: "POST" });await fetch(`${sessionUrl}/decline`, { method: "POST" });These routes are body-less; pair them with the ?token=<caller_token> query param if the bundle issued one for the session. Replace each fetch with AsyncClient session helpers once the Node façade exposes them.
Portal targets, reachability, and session streams
Section titled “Portal targets, reachability, and session streams”get_public_portal_targets lists callable entities for the portal UI; get_public_portal_reachable is a connectivity probe; get_public_session_streams returns WebRTC routing (PeerJS host/port/path, ICE config) needed by the visitor client.
targets = await client.public_access.portal_targets(portal_id)reachable = await client.public_access.portal_reachable(portal_id)streams = await client.public_access.session_streams(session_id)targets, httpResp, err := client.PublicAccessAPI.GetPublicPortalTargets(ctx, portalID).Execute()if err != nil { return err}defer httpResp.Body.Close()_ = targets
reachable, httpResp2, err := client.PublicAccessAPI.GetPublicPortalReachable(ctx, portalID).Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = reachable
streams, httpResp3, err := client.PublicAccessAPI.GetPublicSessionStreams(ctx, sessionID).Execute()if err != nil { return err}defer httpResp3.Body.Close()_ = streamsuse openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let targets = client.public_access().portal_targets(portal_id).await?;let reachable = client.public_access().portal_reachable(portal_id).await?;let streams = client.public_access().session_streams(session_id).await?;const apiBase = "https://api.openapp.house";
const targets = await fetch( `${apiBase}/public/access/portals/${portalId}/targets`,).then((r) => r.json());
const reachable = await fetch( `${apiBase}/public/access/portals/${portalId}/reachable`,).then((r) => r.json() as Promise<{ reachable: boolean }>);
const streams = await fetch( `${apiBase}/public/access/sessions/${sessionId}/streams`,).then((r) => r.json());Fetch streams once when the session reaches active, then feed the PeerJS host / port / path + ICE config into the visitor’s WebRTC client. Replace each fetch with AsyncClient helpers once the Node façade exposes them.
Start a guest session from an invite (POST .../invites/{token}/session)
Section titled “Start a guest session from an invite (POST .../invites/{token}/session)”post_public_invite_session is body-less — the bundle reads everything it needs from inviteToken. Useful for invites that already encode the target entity / mode and just need the visitor to “begin.” Pair it with get_public_session for polling and get_public_session_streams for WebRTC routing.
session = await client.public_access.start_invite_session(token)session_id = session["session_id"]httpResp, err := client.PublicAccessAPI.PostPublicInviteSession(ctx, token).Execute()if err != nil { return err}defer httpResp.Body.Close()The generated client returns only *http.Response for this op — decode the body yourself if you need session_id before polling.
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 session = client .public_access() .start_invite_session(token, &json!({})) .await?;const session = await fetch( `https://api.openapp.house/api/v1/public/access/invites/${token}/session`, { method: "POST" },).then((r) => r.json() as Promise<{ session_id: string; caller_token?: string;}>);
const sessionId = session.session_id;Body-less — the bundle reads target entity / mode from inviteToken. Pair the returned session_id with the polling pattern below. Replace with AsyncClient.startInviteSession once the Node façade exposes it.
Lights and notify-message (portal_lights, session_lights, session_notify_message)
Section titled “Lights and notify-message (portal_lights, session_lights, session_notify_message)”Three POSTs that drive side effects rather than carrying a meaningful response: portal-scoped lights, in-session lights, and an in-session text notification (Web Push to apartment residents). NotifyPortalMessageBody is { "text": string } (only text is required). The Go session-scoped variants take an optional Token(token) query param if your bundle issued a per-session caller token (see caller_token on PublicPortalCreateSessionRequest responses).
await client.public_access.portal_lights(portal_id)await client.public_access.session_lights(session_id)await client.public_access.session_notify_message(session_id, text="Hi, I'm at the door")import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
httpResp, err := client.PublicAccessAPI.PostPublicPortalLights(ctx, portalID).Execute()if err != nil { return err}defer httpResp.Body.Close()
httpResp2, err := client.PublicAccessAPI.PostPublicSessionLights(ctx, sessionID). Token(callerToken). Execute()if err != nil { return err}defer httpResp2.Body.Close()
body := *openapiclient.NewNotifyPortalMessageBody("Hi, I'm at the door")httpResp3, err := client.PublicAccessAPI.PostPublicSessionNotifyMessage(ctx, sessionID). NotifyPortalMessageBody(body). Token(callerToken). Execute()if err != nil { return err}defer httpResp3.Body.Close()Drop Token(...) if the visitor session does not require a per-session caller token.
use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
client.public_access().portal_lights(portal_id, &json!({})).await?;client.public_access().session_lights(session_id, &json!({})).await?;client .public_access() .session_notify_message(session_id, &json!({ "text": "Hi, I'm at the door" })) .await?;The high-level helpers do not currently surface the per-session token query string. If your bundle requires it, drop to transport() + RequestSpec with query = &[("token", caller_token)].
const apiBase = "https://api.openapp.house";
await fetch(`${apiBase}/public/access/portals/${portalId}/lights`, { method: "POST",});
const sessionLightsUrl = new URL( `${apiBase}/public/access/sessions/${sessionId}/lights`,);if (callerToken) sessionLightsUrl.searchParams.set("token", callerToken);await fetch(sessionLightsUrl, { method: "POST" });
const notifyUrl = new URL( `${apiBase}/public/access/sessions/${sessionId}/notify-message`,);if (callerToken) notifyUrl.searchParams.set("token", callerToken);await fetch(notifyUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: "Hi, I'm at the door" }),});Body for notify-message matches NotifyPortalMessageBody (only text is required). Drop the ?token=… query when the visitor session does not require a per-session caller token. Replace each fetch with AsyncClient helpers once the Node façade exposes them.
Poll a session until terminal state
Section titled “Poll a session until terminal state”Sessions are short-lived. The visitor side typically polls get_public_session at 1–2 s intervals while the call is pending / ringing / active, exits when status becomes terminal (accepted + door_opened, declined, cancelled, expired), and surfaces failures to the user. The pattern is the same in every language — use your runtime’s sleep primitive and a bounded retry budget. get_public_session_streams is fetched once after the session reaches active, not on every poll.
import asyncio
TERMINAL = {"accepted", "declined", "cancelled", "expired", "failed"}
async def wait_for_session(client, session_id: str, timeout_s: float = 60.0) -> dict: deadline = asyncio.get_event_loop().time() + timeout_s while True: state = await client.public_access.get_session(session_id) if state.get("status") in TERMINAL: return state if asyncio.get_event_loop().time() >= deadline: raise TimeoutError(f"session {session_id} did not reach terminal state") await asyncio.sleep(1.0)
final = await wait_for_session(client, session_id)import ( "context" "encoding/json" "errors" "time")
terminal := map[string]struct{}{ "accepted": {}, "declined": {}, "cancelled": {}, "expired": {}, "failed": {},}
deadline := time.Now().Add(60 * time.Second)for { state, httpResp, err := client.PublicAccessAPI.GetPublicSession(ctx, sessionID).Execute() if err != nil { return err } httpResp.Body.Close()
raw, _ := json.Marshal(state) var probe struct{ Status string `json:"status"` } _ = json.Unmarshal(raw, &probe) if _, done := terminal[probe.Status]; done { break } if time.Now().After(deadline) { return errors.New("session did not reach terminal state") } time.Sleep(time.Second)}use openapp_sdk::Client;use std::time::{Duration, Instant};use tokio::time::sleep;
const TERMINAL: &[&str] = &["accepted", "declined", "cancelled", "expired", "failed"];
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let deadline = Instant::now() + Duration::from_secs(60);loop { let state = client.public_access().get_session(session_id).await?; let status = state.get("status").and_then(|v| v.as_str()).unwrap_or(""); if TERMINAL.contains(&status) { break; } if Instant::now() >= deadline { return Err("session did not reach terminal state".into()); } sleep(Duration::from_secs(1)).await;}For long-lived guest UIs, prefer a server-side fan-out (e.g. websockets / SSE if your bundle exposes them) over polling — but polling is the lowest-common-denominator that works for every transport.
const TERMINAL = new Set(["accepted", "declined", "cancelled", "expired", "failed"]);
async function waitForSession( apiBase: string, sessionId: string, timeoutMs = 60_000,): Promise<{ status: string }> { const deadline = Date.now() + timeoutMs; for (;;) { const state = (await fetch( `${apiBase}/public/access/sessions/${sessionId}`, ).then((r) => r.json())) as { status: string }; if (TERMINAL.has(state.status)) return state; if (Date.now() >= deadline) throw new Error("session did not reach terminal state"); await new Promise((r) => setTimeout(r, 1_000)); }}
const final = await waitForSession("https://api.openapp.house", sessionId);AsyncClient does not expose baseUrl directly today — pass the API origin as an argument until the Node façade ships getPublicSession (then this becomes client.getPublicSession(sessionId)).