Skip to content
OpenAppPhysical access, fully managed
Login

API keys

API keys (personal access tokens) let third parties use the OpenApp API programmatically without browser-based login. A key acts as the user who created it.

  1. Sign in to the dashboard.
  2. Go to Settings → API keys.
  3. Click Add, pick Organization for new keys, enter a name and expiration.
  4. Copy the token immediately — it is shown only once.

You need the api_keys:create role in the organization (see Roles). Over HTTP, send POST /api/v1/api-keys with the X-Org header set to that organization. Each key consumes one unit of the api_keys quota.

{
"name": "Lobby kiosk",
"expires_in": "P90D",
"scoped_roles": ["entities:set_state"],
"scoped_entity_ids": ["01J8Z3NDEKTSV4RRFFQ69G5FAV"]
}

Send the full token in the X-API-Key header. The API root is {origin}/api/v1, where {origin} is the origin embedded in the token:

Terminal window
curl -H "X-API-Key: https://openapp.example.com_openapp_YOUR_SECRET" \
-H "X-Org: YOUR_ORG_ID" \
https://openapp.example.com/api/v1/devices

Do not send an API key in Authorization: Bearer. The gateway reserves that header for JWTs and OAuth access tokens, and rejects an API key there with 401.

Tokens follow: {origin}_openapp_{secret}

  • origin: The deployment’s bare public origin — scheme, host, and optional port, with no path (for example https://openapp.example.com; production is https://openapp.house). Must match OPENAPP_API_BASE_URL on the server. The SDKs derive the API root {origin}/api/v1 from it and reject a token whose origin contains a path.
  • secret: A random, URL-safe Base64 string.

The full token is shown once at creation. Store it securely.

Every key expires. On create, send exactly one of:

  • expires_at — an absolute RFC 3339 timestamp, for example 2027-01-31T23:59:59Z.
  • expires_in — a duration: ISO 8601 (P90D, PT12H; no months or years) or a compact token (30d, 12h, 2w; M means minutes). 1y is rejected.

Sending both, or neither, is rejected with 400. The expiry must be in the future and at most 3650 days (10 years) away. Expired keys are rejected with 401; create a new key before the old one expires, or extend it with PATCH.

Voice keys (POST /api-keys/voice) default to 365 days and cannot exceed 730 days.

An unscoped key inherits all of its owner’s roles. A scoped key is bounded on every request, whatever its owner holds:

  • scoped_roles — the only roles the key may exercise. The owner’s admin or platform_admin satisfies only roles that are also in this list.
  • scoped_entity_ids — the only entities the key may reach. Entity ids must belong to the key’s organization.

You can only grant roles you hold in the organization. The roles a key may be scoped to are:

entities:set_state, entities:set_hold, entities:read, entities:update, entities:delete, entities:list, entities:create, devices:read, devices:list, integrations:read, integrations:list, integrations:users:list, integrations:users:write, orgs:read, orgs:list, users:read, users:list, zones:read, zones:list.

Any other role string is rejected with 400. Voice keys are always scoped to entities:set_state and one entity. Agent credentials are scoped the same way; the agent’s own allowed_entity_ids is intersected with the key’s entity list, and an agent with no list is not entity-restricted — set one.

A scoped key reaches only the routes below (paths relative to /api/v1), and only when its scoped_roles include the role shown. Every other route returns 403, including /me/*, API key and agent management, integration secrets, and OpenApp Scripting.

RouteRole in scope
GET /statusnone
POST /mcpeach tool checks its own role
GET /orgs, GET /orgs/children, GET /orgs/searchorgs:list
GET /orgs/{id}, GET /orgs/{id}/ancestors, GET /orgs/{id}/permissionsorgs:read
POST /orgs/{org_id}/resolveentities:read
POST /orgs/{org_id}/perception/lpr, POST /orgs/{org_id}/perception/visitor-presententities:set_state
GET /orgs/{id}/users, GET /users/searchusers:list
GET /devicesdevices:list
GET /devices/{id}devices:read
GET /devices/{device_id}/entitiesentities:list
GET /entitiesentities:list
POST /entitiesentities:create
GET /entities/{id}, GET /entities/{id}/metadata-definition, GET /entities/{id}/holdentities:read
PUT /entities/{id}, PATCH /entities/{id}entities:update
DELETE /entities/{id}entities:delete
PUT /entities/{id}/holdentities:set_hold
POST /entities/{id}/actions/{action_id} and .../previewentities:set_state for switchable.*; entities:set_hold for switchable.set_relay_mode. Other actions are refused.
GET /integrationsintegrations:list
GET /integrations/{id}, GET /integrations/{id}/entities, GET /integrations/{id}/access-invitesintegrations:read
GET /integrations/{id}/integration-usersintegrations:users:list
POST /integrations/{id}/ops/users_admin.list_usersintegrations:users:list
POST /integrations/{id}/ops/users_admin.remove_userintegrations:users:write
GET /zones/{id}, GET /zones/{id}/summaryzones:read
GET /zones, GET /zones/treezones:list

The role in the table admits the request; the handler then runs its normal permission checks, bounded by the same scope.

When scoped_entity_ids is set:

  • Routes with an entity {id} in the path work only for listed entities.
  • Routes that reach entities or devices not named in the path are refused with 403: GET /devices, GET /devices/{id}, GET /devices/{device_id}/entities, GET /entities, POST /entities, GET /integrations/{id}/entities, and POST /orgs/{org_id}/resolve.
  • Hosted MCP and perception routes check each entity they touch against the list.

A key with an entity list but no scoped_roles can do nothing.

PATCH /api/v1/api-keys/{id} renames a key and optionally changes its expiry:

{ "name": "Lobby kiosk (2027)", "expires_in": "P180D" }
  • name is required.
  • expires_at or expires_in (not both) sets a new expiry, which must be in the future. Omit both to keep the current expiry. expires_in counts from now.
  • Returns 204. Scope cannot be changed; create a new key instead.

You can only update your own keys. A scoped key cannot call this route.

API keys follow a two-stage deactivation model: revoke first, delete (optional) later.

  1. Active – Key can authenticate API requests.
  2. Revoked – Key is disabled; any request using it returns 401. The key remains in the database with a revoked_at timestamp.
  3. Deleted – Key is permanently removed from the database. Only revoked keys can be deleted.

When to use: You suspect a key was exposed, the key is no longer needed, or an employee or service no longer requires access.

Effect: The key stops working immediately. The row stays in the database so you can see when it was revoked. You can restore it later (if not expired) or delete it.

How: Dashboard (Settings → API keys → Revoke) or DELETE /api/v1/api-keys/{id}.

When to use: A key was revoked by mistake, or you want to re-enable it (for example after a temporary suspension).

Effect: Clears revoked_at; the key works again.

How: Dashboard (Settings → API keys → Restore on a revoked key) or POST /api/v1/api-keys/{id}/restore. Returns 409 if the key is not revoked or is expired.

When to use: You want to remove revoked keys from the database (cleanup, privacy, or storage reduction).

Effect: The key row is permanently removed. This cannot be undone.

How: Dashboard (Settings → API keys → Delete on a revoked key) or DELETE /api/v1/api-keys/{id}/purge. Returns 409 if the key is not revoked.

ActionWhen to useReversible?
RevokeKey compromised, no longer needed, temporary suspensionYes – use Restore to re-enable
RestoreUndo revokeN/A – key is active again
DeleteRemove revoked key from databaseNo – row is gone

Typical flow: Create → use → Revoke (when no longer needed) → Restore (if needed) or Delete (for cleanup).

Every route below refuses scoped keys (403).

  • Create: POST /api/v1/api-keys – requires X-Org; returns the token once
  • Create voice key: POST /api/v1/api-keys/voice – one entity, entities:set_state
  • List: GET /api/v1/api-keys – your keys, active and revoked, paginated
  • Rename or extend: PATCH /api/v1/api-keys/{id} – name, optional expires_at or expires_in
  • Revoke: DELETE /api/v1/api-keys/{id} – disables the key
  • Restore: POST /api/v1/api-keys/{id}/restore – re-enables a revoked key (409 if not revoked or expired)
  • Delete (purge): DELETE /api/v1/api-keys/{id}/purge – permanently removes a revoked key (409 if not revoked)
  • Never commit tokens to source control.
  • Use short expiries and extend them, rather than long-lived keys.
  • Use scoped keys whenever the key does not need everything you can do.
  • Revoke keys that may have been exposed.