# Add users to access control group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/add-users-to-access-control-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/addUsersToAccessControlGroup Adds specified users to an access control group # Assign access control credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/assign-access-control-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/assignAccessControlCredential Assign a currently unassigned credential to a user # Create 64-bit raw Wiegand credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-64-bit-raw-wiegand-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createWiegand64BitRawCredential Create a 64bit raw wiegand credential. Used when an explicit endpoint for the wiegand format is not otherwise defined. # Create access control credential by hex value and type Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-access-control-credential-by-hex-value-and-type https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createAccessControlCredentialByHexValueAndType Create a credential based on a hex value and credential type. Useful if you see an unauthorized badge event, know its type and want to import it # Create access control group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-access-control-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createAccessControlGroup Creates an access control group # Create access grant Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-access-grant https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createAccessGrant Create a location access grant # Create access revocation Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-access-revocation https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createAccessRevocation Create a location access revocation # Create an HID Corp1000 Standard 35 bit credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-an-hid-corp1000-standard-35-bit-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/create35BitCorp1000StdCredential Create a HID Corp1000 Standard 35 bit credential # Create an HID Corp1000 Standard 48 bit credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-an-hid-corp1000-standard-48-bit-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/create48BitCorp1000StdCredential Create a HID Corporate 1000 Standard 48 bit credential # Create D10202 Wiegand credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-d10202-wiegand-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createWiegandD10202Credential Create a D10202 wiegand credential # Create H10301 Wiegand credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-h10301-wiegand-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createWiegandH10301Credential Create a H10301 wiegand credential # Create H10302 Wiegand credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-h10302-wiegand-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createWiegandH10302Credential Create a H10302 wiegand credential # Create H10304 Wiegand credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-h10304-wiegand-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createWiegandH10304Credential Create a H10304 wiegand credential # Create Rhombus Secure CSN credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-rhombus-secure-csn-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createRhombusSecureCsnCredential Create a rhombus secure csn credential (Rhombus Badge) # Create standard CSN credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-standard-csn-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createStandardCsnCredential Create a standard csn credential (Third Party Badge) # Create Wiegand credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/create-wiegand-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/createWiegandCredential Create a wiegand credential # Delete access control credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/delete-access-control-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/deleteAccessControlCredential Delete an access control credential. It is recommended to revoke a credential rather than delete it to preserve credential history # Delete access control group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/delete-access-control-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/deleteAccessControlGroup Delete an access control group # Delete location access grant Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/delete-location-access-grant https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/deleteLocationAccessGrant Deletes a location access grant # Delete location access revocation Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/delete-location-access-revocation https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/deleteLocationAccessRevocation Deletes a location access revocation # Delete unassigned access control credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/delete-unassigned-access-control-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/deleteUnassignedAccessControlCredential Delete an unassigned access control credential. The credential must be unassigned (revoked) before this method can be called. History of the credential is maintained but it will no longer be returned as an unassigned credential. # Find access control credentials by guest pass Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-credentials-by-guest-pass https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlCredentialByGuestPass Find all access control credentials assigned to the specified guest pass # Find access control credentials by org Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-credentials-by-org https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlCredentialByOrg Find all access control credentials in the org # Find access control credentials by user Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-credentials-by-user https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlCredentialByUser Find all access control credentials for the specified user # Find access control credentials by users Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-credentials-by-users https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlCredentialByUsers Find all access control credentials for the specified users # Find access control group by exact name Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-group-by-exact-name https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlGroupByExactName Retrieve the access control group with the specified name # Find access control group memberships by user Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-group-memberships-by-user https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlGroupMembershipsByUser Find all access control group memberships by user # Find access control groups by name prefix Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-groups-by-name-prefix https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlGroupsByNamePrefix Retrieve all access control groups with a name starting with the specified prefix # Find access control groups by org Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-groups-by-org https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlGroupsByOrg Retrieve all access control groups defined in the org # Find access control groups by user membership Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-access-control-groups-by-user-membership https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAccessControlGroupsByUserMembership Find all access control groups a user belongs to # Find all users for access control group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-all-users-for-access-control-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findAllUsersForAccessControlGroup Find all users belonging to an access control group # Find credential history by credential hex value Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-credential-history-by-credential-hex-value https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findCredentialHistoryByCredentialHexValue Retrieves all credentials that have owned the credential hex value. Expect at most 1 valid/active credential and the rest are revoked # Find credential history by credential value Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-credential-history-by-credential-value https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findCredentialHistoryByCredentialValue Retrieves all credentials that have owned the credential value. # Find credential history by user Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-credential-history-by-user https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findCredentialHistoryByUser Retrieves all credentials both current and revoked that were at some point assigned to the specified user # Find location access grants by access controlled door Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-access-controlled-door https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByAccessControlledDoor Finds location access grants by the specified access controlled door # Find location access grants by access controlled elevator landing Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-access-controlled-elevator-landing https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByAccessControlledElevatorLanding Finds location access grants by the specified access controlled elevator landing # Find location access grants by door label Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-door-label https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByDoorLabel Finds location access grants by the specified door label # Find location access grants by group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByGroup Finds location access grants by the specified group # Find location access grants by location Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-location https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByLocation Finds location access grants by the specified location # Find location access grants by location and user Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-location-and-user https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByLocationAndUser Finds location access grants by the specified location and user # Find location access grants by org Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-org https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByOrg Finds all location access grants in the org # Find location access grants by user Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-grants-by-user https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessGrantsByUser Finds location access grants by the specified user # Find location access revocations by access controlled door Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-revocations-by-access-controlled-door https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessRevocationsByAccessControlledDoor Finds location access revocations by the specified access controlled door # Find location access revocations by access controlled elevator landing Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-revocations-by-access-controlled-elevator-landing https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessRevocationsByAccessControlledElevatorLanding Finds location access revocations by the specified access controlled elevator landing # Find location access revocations by door label Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-revocations-by-door-label https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessRevocationsByDoorLabel Finds location access revocation by the specified door label # Find location access revocations by group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-revocations-by-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessRevocationsByGroup Finds location access revocations by the specified group # Find location access revocations by org Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-revocations-by-org https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessRevocationsByOrg Finds all location access revocations in the org # Find location access revocations by user Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/find-location-access-revocations-by-user https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/findLocationAccessRevocationsByUser Finds location access revocations by the specified user # Get access control credential details Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/get-access-control-credential-details https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/getAccessControlCredentialDetails Retrieves an access control credential of any type, including sensitive details, by UUID. # Get location access grant Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/get-location-access-grant https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/getLocationAccessGrant Retrieve a location access grant by id # Get location access revocation Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/get-location-access-revocation https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/getLocationAccessRevocation Retrieve a location access revocation by id # Get Rhombus Secure CSN credential details Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/get-rhombus-secure-csn-credential-details https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/getRhombusSecureCsnCredentialDetails Retrieves a rhombus secure csn credential including sensitive details. Deprecated: use getAccessControlCredentialDetails. # Get standard CSN credential details Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/get-standard-csn-credential-details https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/getStandardCsnCredentialDetails Retrieves a standard csn credential including sensitive details. Deprecated: use getAccessControlCredentialDetails. # Remove users from access control group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/remove-users-from-access-control-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/removeUsersFromAccessControlGroup Removes specified users from an access control group # Revoke access control credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/revoke-access-control-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/revokeAccessControlCredential Revokes an access control credential. Unlike suspension this will give up its claim on the credential value allowing another cred to claim it as its identifier # Suspend access control credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/suspend-access-control-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/suspendAccessControlCredential Mark an access control credential as suspended # Unlock access controlled door Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/unlock-access-controlled-door https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/unlockAccessControlledDoor Unlock an access controlled door # Unsuspend access control credential Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/unsuspend-access-control-credential https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/unsuspendAccessControlCredential Mark an access control credential as no longer suspended # Update access control group Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/update-access-control-group https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/updateAccessControlGroup Updates an access control group's meta information like name and description # Update access grant Source: https://api-docs.rhombus.community/api-reference/access-control-webservice/update-access-grant https://api2.rhombussystems.com/api/openapi/public.json post /api/accesscontrol/updateAccessGrant Updates a location access grant # API Reference Source: https://api-docs.rhombus.community/api-reference/overview Complete REST API reference covering 900+ Rhombus endpoints for cameras, access control, sensors, alarms, video streaming, and analytics. The Rhombus API gives you programmatic access to the full Rhombus platform across 900+ endpoints. Built on an API-first architecture since 2016, every endpoint you see here is the same one that powers the Rhombus web console, mobile apps, and firmware. If you can do it in the Rhombus UI, you can do it through the API. ## Authentication Every request requires two headers for token-based authentication: | Header | Value | | --------------- | ---------------------------------------------------------------------------------------------------- | | `x-auth-scheme` | `api-token` | | `x-auth-apikey` | Your API key from the [Rhombus Console](https://console.rhombussystems.com/settings/api-management/) | ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/org/getOrganization \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```python Python theme={null} import requests response = requests.post( "https://api2.rhombussystems.com/api/org/getOrganization", headers={ "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, json={}, ) print(response.json()) ``` ## Base URL All API requests are made to one of two base URLs depending on the type of operation. Each has a US and an EU variant — use the one that matches your organization's [region](/api-regions): | Purpose | US | EU | | ------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------- | | All API endpoints (device management, configuration, events, users, etc.) | `https://api2.rhombussystems.com` | `https://api2.eu.rhombussystems.com` | | Media and streaming operations (video clips, live streams, thumbnails) | `https://media.rhombussystems.com` | `https://media.eu.rhombussystems.com` | API keys are region-bound — a key generated in one region's console returns `403 Invalid api key` against the other region's endpoints. See [API Regions (US & EU)](/api-regions). ## Common Patterns * **All requests use POST** — every endpoint accepts a `POST` request with a JSON body, even for read operations. * **All responses are JSON** — responses follow a consistent JSON structure across the entire API. * **Empty body for simple queries** — endpoints that do not require parameters still expect an empty JSON object (`{}`). ## Browse by Category Manage cameras, retrieve clips, configure video settings, and access live and recorded footage. Control audio devices, manage audio clips, and configure speaker settings. Manage doors, access points, credentials, and entry logs. Monitor environmental sensors, retrieve readings, and configure thresholds. Query events, configure alert rules, and manage notification preferences. Access face recognition, license plate detection, people counting, and other AI-powered features. Manage users, roles, permissions, and organization settings. Configure webhooks, third-party connections, and external system integrations. System configuration, firmware management, network settings, and platform administration. This reference is generated directly from our production OpenAPI spec, which is fetched fresh on every site build. We also check the upstream API for changes several times a day and redeploy automatically, so the reference stays current. Use the interactive playground on any endpoint page to test requests with your API key. # API Regions (US & EU) Source: https://api-docs.rhombus.community/api-regions Connect to the right Rhombus API region — US and EU endpoint URLs, region-bound API keys, CLI region configuration, and cross-region troubleshooting. ## One organization, one region Rhombus operates two independent production regions: **US** and **EU**. Your organization — its users, devices, API keys, and data — lives in exactly one of them. The region determines which hostnames your integration connects to; everything else is identical, including endpoint paths, authentication headers, request and response formats, and [rate limits](/rate-limits). **API keys are region-bound.** A key generated in the EU Console only works against EU endpoints, and a US key only works against US endpoints. There is no cross-region redirect — see [Troubleshooting](#troubleshooting) below. ## Regional endpoints | Purpose | US | EU | | ----------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | API base URL | `https://api2.rhombussystems.com` | `https://api2.eu.rhombussystems.com` | | Media & streaming | `https://media.rhombussystems.com` | `https://media.eu.rhombussystems.com` | | Rhombus Console | `https://console.rhombussystems.com` | `https://console.eu.rhombussystems.com` | | OAuth token host | `https://auth-web.rhombussystems.com` | `https://auth-web.eu.rhombussystems.com` | | WebSocket (STOMP) | `wss://ws.rhombussystems.com:8443/websocket` | `wss://ws.eu.rhombussystems.com:8443/websocket` | | OpenAPI spec | [`/api/openapi/public.json`](https://api2.rhombussystems.com/api/openapi/public.json) | [`/api/openapi/public.json`](https://api2.eu.rhombussystems.com/api/openapi/public.json) | The pattern is consistent across the platform: the EU hostname inserts `.eu.` after the subdomain — `api2.rhombussystems.com` becomes `api2.eu.rhombussystems.com`. ## Which region is your organization in? Check the address bar when you're signed in to the Rhombus Console: * `console.rhombussystems.com` — your organization is in the **US** region. * `console.eu.rhombussystems.com` — your organization is in the **EU** region (the sign-in page is also badged **EU**). Generate API keys in your own region's console under **Settings → API Management** — for example, EU organizations use [console.eu.rhombussystems.com/settings/api-management](https://console.eu.rhombussystems.com/settings/api-management/). ## Call the API in your region Set the base URL once and every code sample in these docs works for either region — the paths, headers, and payloads never change. ```bash cURL theme={null} # US organizations BASE_URL="https://api2.rhombussystems.com" # EU organizations # BASE_URL="https://api2.eu.rhombussystems.com" curl -X POST "$BASE_URL/api/org/getOrganization" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```python Python theme={null} import requests # US: "https://api2.rhombussystems.com" # EU: "https://api2.eu.rhombussystems.com" BASE_URL = "https://api2.rhombussystems.com" response = requests.post( f"{BASE_URL}/api/org/getOrganization", headers={ "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, json={}, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} // US: "https://api2.rhombussystems.com" // EU: "https://api2.eu.rhombussystems.com" const BASE_URL = "https://api2.rhombussystems.com"; const response = await fetch(`${BASE_URL}/api/org/getOrganization`, { method: "POST", headers: { "Content-Type": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", }, body: JSON.stringify({}), }); console.log(await response.json()); ``` A successful call returns `200 OK` with your organization's details. A `403 Forbidden` with `"Invalid api key"` usually means the key and the base URL belong to different regions — see [Troubleshooting](#troubleshooting). ### Generated SDKs Each region serves the OpenAPI spec at the same path (`/api/openapi/public.json`). The spec's `servers` entry currently lists only the US base URL, so when you generate a client for an EU organization, override the base URL explicitly: ```bash Generate an EU client theme={null} openapi-generator-cli generate \ -i https://api2.eu.rhombussystems.com/api/openapi/public.json \ -g python \ -o ./rhombus-python-client # Then point the generated client at the EU host, e.g.: # configuration.host = "https://api2.eu.rhombussystems.com" ``` ## Rhombus CLI The [Rhombus CLI](/rhombus-cli) has built-in region support. `rhombus configure` asks which region your organization is in at the `Region (us/eu)` prompt and derives the endpoint URL from your answer, and `rhombus login` automatically uses the matching regional sign-in and token hosts. You can also point any single command — or your whole environment — at the EU endpoint directly: ```bash CLI region overrides theme={null} # Per command rhombus camera get-minimal-camera-state-list \ --endpoint-url https://api2.eu.rhombussystems.com # Per shell session export RHOMBUS_ENDPOINT_URL=https://api2.eu.rhombussystems.com rhombus camera get-minimal-camera-state-list ``` ## OAuth and WebSocket connections Regional hostnames apply to every part of the platform, not just the REST API: * **[Sign in with Rhombus (OAuth)](/oauth-authentication)** — the three hosts in the OAuth flow (console, `auth-web`, `api2`) all switch to their `.eu.` counterparts for EU organizations. OAuth applications are region-bound just like API keys: register, authorize, and exchange tokens all in the same region. * **[WebSocket real-time events](/websocket/overview)** — EU organizations connect to `wss://ws.eu.rhombussystems.com:8443/websocket`. Authentication and the STOMP protocol are identical. ## Data residency Organizations provisioned in the EU region are hosted in the European Union: their API traffic, video storage, and cloud processing stay in-region. ## Troubleshooting ### `403 Invalid api key` — but the key is correct A request to the wrong region's endpoint fails with: ```json theme={null} { "msg": "Invalid api key", "error": "Forbidden", "status": 403 } ``` This response is identical whether the key is genuinely invalid or simply belongs to the other region — there is no region hint in the error. If you're confident the key is correct, check which console it was generated in (`console.` vs `console.eu.`) and make sure your base URL matches. ### OAuth: "This application is not available for your organization" OAuth `clientId`s are region-bound. If your app was registered in one region and a user tries to authorize it through the other region's console, the consent screen shows **This application is not available for your organization**. Make sure the registration call, the authorization URL, and the token exchange all target the same region as the signing-in user's organization. ## Next steps Browse 900+ endpoints — every path works identically in both regions. Configure the CLI for your region and explore the API from your shell. Stream real-time events from your region's WebSocket endpoint. The same limits apply in both regions, enforced within your organization's region. # Changelog Source: https://api-docs.rhombus.community/changelog Release notes for the Rhombus API — track new endpoints, breaking changes, deprecations, SDK updates, and platform improvements over time. ## US & EU API regions documented Rhombus operates two independent production regions — US and EU — and the documentation now covers both. ### New features **API Regions guide** — A new [API Regions (US & EU)](/api-regions) guide covers the full regional endpoint map (`api2.eu.rhombussystems.com`, `media.eu.rhombussystems.com`, `console.eu.rhombussystems.com`, `auth-web.eu.rhombussystems.com`, `wss://ws.eu.rhombussystems.com:8443/websocket`), how to tell which region your organization is in, region-bound API keys, and cross-region troubleshooting — including why a wrong-region call returns `403 Invalid api key`. **EU endpoints throughout the docs** — The quickstart, API Reference overview, OAuth guide, partner guide, WebSocket pages, rate limits, and CLI documentation now show both regions' hosts. The WebSocket AsyncAPI definition gained a `production-eu` server entry. **Rhombus CLI region support documented** — `rhombus configure` prompts for a region (`us`/`eu`) and derives the endpoint automatically; `rhombus login` uses the matching regional sign-in and token hosts. Previously undocumented. [Read the guide](/api-regions) *** **Updated**: 2026-08-03 ## EdgeCaster: sub-second latency, unlimited streams, and 24/7 self-healing The [EdgeCaster RTSP gateway](/implementations/edgecaster-rtsp) received a major update focused on efficiency and sub-second latency. ### Enhancements **Sub-second latency** — The stream-copy pipeline (no transcoding) was retuned to strip buffering at every hop: FFmpeg runs with no input buffering, low-delay flags, fast startup, and zero mux delay, while MediaMTX drops stalled publishers quickly. EdgeCaster always pushes the latest frame with sub-second added latency inside the device; end-to-end latency also depends on the camera's keyframe interval and your consumer's own buffer. **Unlimited concurrent streams** — The previous 10-stream cap is gone. Streams are uncapped by default (`max_streams: 0`); the device runs as many as its network and CPU allow. Real-world testing on a Raspberry Pi 5 showed a handful of 1080p streams plus multiple RTSP readers at roughly 3% CPU. **24/7 self-healing** — Frame-level stall detection spots frozen-but-alive feeds within seconds and recovers them through a single guarded relaunch with a fresh Secure Raw Stream URL. Fast retries, then a 5-minute backoff — it never permanently gives up. **Simpler install** — A one-line installer for any Ubuntu/Debian machine (arm64, amd64, or armv7) and a ready-to-flash Raspberry Pi image. Manual git-checkout installs now receive nightly auto-updates too. **Redesigned, mobile-ready dashboard** — A cleaner, responsive operator console that works on any device or browser. ### New features **Live health dashboard** — Real-time metrics over Server-Sent Events (\~1s): active streams, CPU, memory, temperature, power/throttle status, load average, uptime, and per-stream throughput and reader counts. Pi temperature and power metrics require the `linux-raspi` kernel. **Webhook alerts** — Stream-down, under-voltage, thermal-throttling, and sustained high-CPU/load alerts to Slack, Make.com, or any HTTP listener, with configurable thresholds, per-alert cooldowns, and clear-on-recovery. Configure under Settings → Alerts, including a test-alert button. **Live logs viewer** — Stream Application, Streams, and Rhombus API logs live from the dashboard in a terminal-style console — no SSH needed. **One-click secure public access** — Reach the dashboard from anywhere via a Cloudflare quick tunnel (no Cloudflare account required), protected by a username and password. LAN access stays login-free; the public link is ephemeral and changes each time public access restarts, and RTSP is never tunneled. [Read the guide](/implementations/edgecaster-rtsp) *** **Updated**: 2026-07-10 ## New implementation guides and a full accuracy pass ### New features **Access Control** — A comprehensive guide to the access control API: listing and managing access-controlled doors, remote unlock, Wiegand credentials, access groups and grants, weekly schedules, and lockdown plans. [Read the guide](/implementations/access-control) **License Plate Recognition & Vehicle** — Query LPR detection events, search license plates (including fuzzy matching), build vehicle watchlists with alert and trust flags, label vehicles, and export detections. [Read the guide](/implementations/lpr-vehicle) **Face Recognition & People Counting** — Enroll and manage known people, upload face images for matching, query face-match events, tune the match confidence threshold, and pull camera people-counting analytics. [Read the guide](/implementations/faces-people-counting) **Reports & Analytics** — Generate count and time-series reports, read occupancy and line-crossing counts, work with the report type/interval/scope enums, and export report data as CSV. [Read the guide](/implementations/reports-analytics) ### Updates **Documentation accuracy pass** — Every guide was reviewed against the production API and corrected for endpoint paths, request and response fields, authentication headers, and event payloads. Notable corrections include the "Sign in with Rhombus" OAuth 2.0 flow, the webhook delivery payload structure, and the WebSocket event schema. *** **Updated**: 2026-07-08 ## Partner API documentation ### New features **Partner API Calls** — A new guide for managed service providers and resellers covering how to make API calls as a partner: authenticating with a partner API key, managing client organizations with partner endpoints, and scoping client-level calls to a managed org using the `x-auth-org` header. [Read the guide](/partner-api-calls) *** **Updated**: 2026-06-29 ## Developer tools and integrations ### New features **Rhombus CLI** — A command-line tool for managing your entire Rhombus deployment from the terminal. Supports 60+ API resource categories, video stitching, frame analysis, real-time alert monitoring, and AI-powered chat. Available via Homebrew, shell script, or from source. [Read the guide](/rhombus-cli) **React SDK** — The official [`@rhombussystems/react`](https://www.npmjs.com/package/@rhombussystems/react) package provides drop-in components for embedding Rhombus camera streams in your React applications. Includes both a DASH buffered player and a realtime H.264 WebSocket player. [Read the guide](/implementations/react-sdk) **Rhombus API MCP Server** — An MCP server that gives AI tools like Claude, Cursor, and others direct access to your cameras, access control, sensors, and the full Rhombus platform. Supports 30+ tools across all major resource types. [Read the guide](/rhombus-mcp) **Claude Code Plugins** — Ready-made skills, agents, and hooks for Claude Code organized by persona (developer, user, partner). Install from the [Rhombus Claude Code Plugin Marketplace](https://github.com/RhombusSystems/claude-code-plugins). [Read the guide](/claude-code-plugins) **EdgeCaster RTSP Gateway** — A Raspberry Pi edge gateway that converts Rhombus secure camera streams to RTSP for legacy VMS, NVR, and third-party video systems. [Read the guide](/implementations/edgecaster-rtsp) **Official n8n community node** — The [`@rhombussystems/n8n-nodes-client`](https://www.npmjs.com/package/@rhombussystems/n8n-nodes-client) package provides native Rhombus access in n8n with 6 resource types and 19 operations — no raw HTTP configuration needed. [Read the guide](/low-code-no-code/n8n) **OAuth 2.0 authentication** — You can now authenticate Rhombus users in your application using OAuth 2.0 with PKCE ("Sign in with Rhombus"). [Read the guide](/oauth-authentication) **SAML SSO and SCIM provisioning** — Programmatically configure SAML single sign-on and SCIM user provisioning for your Rhombus organization, with support for Okta, Azure AD, Google Workspace, and OneLogin. [Read the guide](/implementations/saml-sso-provisioning) ### Updates **API Reference navigation** — The API Reference sidebar now organizes 800+ endpoints into 7 collapsible groups (Cameras & Video, Access Control, Security & Alerts, IoT Sensors, Organization & Users, Integrations, Devices & System) for faster browsing. **Rate limits guide** — New documentation covering how Rhombus API rate limiting works, including retry strategies and code examples. [Read the guide](/rate-limits) **WebSocket documentation** — Comprehensive rewrite of the real-time event streaming guides covering authentication, connection lifecycle, STOMP protocol, event monitoring, and troubleshooting. [Read the guides](/websocket/overview) **Improved implementation guides** — The [alarm monitoring](/implementations/alarm-monitoring) and [IoT sensors](/implementations/iot-sensors) guides now include complete API workflows with multi-language code examples. *** **Updated**: 2026-04-20 ## Major API Expansion The Rhombus API has been updated with **29 new endpoints** added across **2 new service categories**. ### New Endpoints **Relay** (24 endpoints): * NVR management: details, state, uptime, reboot, unregister, firmware updates * Third-party camera integration: assign, authenticate, discover, manage passwords, RTSP endpoints * PTZ controls: move and status **Access Control Integrations** (4 endpoints): * Boulevard integration: create, read, update, delete, and connection testing ### New Service Categories * **Relay Webservice** *** **API Version**: 1.0 **Updated**: 2026-04-02 ## New API Endpoints The Rhombus API has been updated with **2 new endpoints** added. ### New Endpoints **Component**: * `POST /api/component/findPaginatedComponentEventsByAccessControlledDoor` - Retrieve all component events relevant to the specified AccessControlledDoor **Report**: * `POST /api/report/getOccupancyCountsV2` - Get occupancy counts V2 *** **API Version**: 1.0 **Updated**: 2026-03-07 ## New API Endpoints The Rhombus API has been updated with **2 new endpoints** added. ### New Endpoints **Report**: * `POST /api/report/getBatchThresholdCrossingCountReport` - Get batch threshold crossing count report * `POST /api/report/getThresholdCrossingCountReportForOrg` - Get org wide batch threshold crossing count report *** **API Version**: 1.0 **Updated**: 2026-02-10 ## New API Endpoints The Rhombus API has been updated with **5 new endpoints** added. ### New Endpoints **Elevator**: * `POST /api/component/elevator/addAccessControlledElevatorLandingLabel` - Create a label that can be assigned to an access controlled elevator landing * `POST /api/component/elevator/findAccessControlledElevatorLandingShadows` - Find access controlled elevator landing shadows within the org * `POST /api/component/elevator/findAccessControlledElevatorLandingShadowsByLocation` - Find access controlled elevator landing shadows within the specified location * `POST /api/component/elevator/getAccessControlledElevatorLandingLabelsForOrg` - Get all access controlled elevator landing labels for the organization * `POST /api/component/elevator/removeAccessControlledElevatorLandingLabel` - Remove a label to an access controlled elevator landing *** **API Version**: 1.0 **Updated**: 2026-01-30 ## Documentation Navigation Restructure Reorganized the documentation into topic-based navigation groups for easier discovery. * **Guides**: Getting started, implementation guides for cameras, access control, alarm monitoring, IoT sensors, and more * **Integrations**: Low-code/no-code platforms (Zapier, Make.com, N8N) and AI tool integration via MCP * **API Reference**: Complete REST API reference with live endpoint testing, auto-generated from the OpenAPI spec All existing links continue to work — no breaking changes to content or URLs. ## New API Endpoints The Rhombus API has been updated with **2 new endpoints** added. ### New Endpoints **Alert Monitoring**: * `POST /api/alertmonitoring/disableMonitoringForLocation` - Disable monitoring for location * `POST /api/alertmonitoring/enableMonitoringForLocation` - Enable monitoring for location ## New Documentation Launch Complete redesign of the Rhombus Developer Documentation, now powered by [Mintlify](https://mintlify.com). * **New platform**: Migrated to Mintlify with improved search, dark mode, and mobile support * **3-tab navigation**: Guides, Integrations, and API Reference for clear content separation * **Interactive API playground**: Live endpoint testing directly in the documentation * **Nightly sync**: Automated pipeline keeps API documentation current with production * **AI-ready**: MCP server integration, `llms.txt` context files, and downloadable OpenAPI spec Visit [rhombus.community](https://rhombus.community) to share feedback or request new content. # Claude Code Plugins Source: https://api-docs.rhombus.community/claude-code-plugins Install Rhombus skills, sub-agents, and hooks for Claude Code to build integrations, manage devices, and monitor alerts from your terminal. The [Rhombus Claude Code Plugin Marketplace](https://github.com/RhombusSystems/claude-code-plugins) provides ready-made skills, sub-agents, and hooks for Claude Code, organized by persona. Each plugin gives Claude Code deep knowledge of the Rhombus platform so it can help you build integrations, manage devices, and monitor alerts directly from your terminal. ## Quick Start In Claude Code, run: ```text theme={null} /plugin marketplace add RhombusSystems/claude-code-plugins ``` Enable the plugin that matches your role: ```text theme={null} /plugin enable rhombus-developer ``` Or for other roles: ```text theme={null} /plugin enable rhombus-user /plugin enable rhombus-partner ``` You can enable multiple plugins if needed. Trigger commands and skills with slash commands: ```text theme={null} /rhombus-find-endpoint unlock door /code-review src/auth.ts /rhombus-alerts last 24 hours ``` ## Available Plugins For engineers building on the Rhombus platform. Auto-configures the official `rhombus-node-mcp` and Rhombus docs MCP servers. ### Commands | Command | Description | | ------------------------ | -------------------------------------------------------------------------------------- | | `/rhombus-find-endpoint` | Search the OpenAPI spec for endpoints by keyword, tag, or operationId | | `/rhombus-schema` | Dump the request and response schema for a specific endpoint as markdown | | `/rhombus-curl` | Generate a ready-to-run cURL command with auth headers and a skeleton request body | | `/rhombus-sdk` | Generate a typed SDK client (Python, TypeScript, Java, Go) via `openapi-generator-cli` | | `/rhombus-mcp-status` | Report the status of the Rhombus MCP servers and diagnose connection issues | | `/rhombus-newproject` | Scaffold a new integration project with a typed SDK, webhook receiver, and README | ### Skills | Skill | Description | | -------------------------- | ------------------------------------------------------------------------------------------------ | | `rhombus-api` | Full Rhombus API reference — 900+ endpoints across 65+ service categories, plus the OpenAPI spec | | `rhombus-sdk-codegen` | Generate typed SDK clients (Python, TypeScript, Java, Go, C#) from the OpenAPI spec | | `rhombus-webhook-receiver` | Scaffold a webhook listener in Express, FastAPI, or AWS Lambda | | `rhombus-edge-streaming` | Edge streaming and third-party camera integration — RTSP, ONVIF, and analytics seekpoints | | `api-doc` | Generate API documentation from your source code | | `code-review` | Structured code review for quality, security, performance, and Rhombus conventions | ### Agents | Agent | Description | | -------------------------- | ------------------------------------------------------------------------------------------------ | | `rhombus-api-architect` | Designs integrations, maps endpoint chains, and weighs CLI/API/MCP/SDK trade-offs | | `rhombus-webhook-debugger` | Diagnoses misbehaving webhooks — not firing, duplicates, signature failures, unexpected payloads | ### Hooks | Hook | Trigger | Description | | --------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------- | | `rhombus-api-intercept` | PreToolUse (Bash) | Suggests the `rhombus` CLI or SDK when it detects raw `curl`/`wget`/`fetch` calls to Rhombus API domains | | `rhombus-openapi-freshness` | SessionStart | Warns when the bundled OpenAPI spec is stale (older than 90 days) | ### Example Usage ```text theme={null} # Find the endpoint you need /rhombus-find-endpoint unlock door # Generate a typed SDK client /rhombus-sdk typescript # Review code for security and performance issues /code-review src/auth.ts ``` For day-to-day Rhombus platform users. Installs and keeps the Rhombus CLI up to date automatically. ### Commands | Command | Description | | -------------------------- | --------------------------------------------------------------------------------------- | | `/rhombus-alerts` | Fetch and summarize recent alerts, optionally filtered by camera and time window | | `/rhombus-analyze` | Run `rhombus analyze` on an alert or a camera + time window and summarize the activity | | `/rhombus-status` | One-shot deployment health report — cameras, door states, sensor battery, recent issues | | `/rhombus-watch` | Open a Rhombus camera player in your browser | | `/rhombus-context-refresh` | Regenerate the local deployment context — index, manifest, and per-camera stills | ### Skills | Skill | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------- | | `rhombus-cli` | Rhombus CLI reference — install, auth, profiles, global flags, and the API service groups | | `rhombus-deployment-context` | Generate, query, and analyze deployment context; covers `rhombus context`, `analyze`, and `stitch` | | `rhombus-mind` | Interact with Rhombus MIND, the natural-language AI assistant (`rhombus chat` and `rhombus voice`) | | `rhombus-support-links` | Curated directory of Rhombus support articles for setup and administration topics | ### Agents | Agent | Description | | ------------------------------ | ------------------------------------------------------------------------------------------- | | `rhombus-cli` | CLI assistant — builds, executes, and troubleshoots `rhombus` commands | | `rhombus-alerts` | Alert monitoring specialist — investigates security events, downloads footage | | `rhombus-devices` | Device management — inventories cameras, sensors, and doors, and audits deployments | | `rhombus-footage-investigator` | Reconstructs incidents across cameras with `analyze` and `stitch`, and builds review videos | ### Hooks | Hook | Trigger | Description | | ---------------------- | ----------------- | ---------------------------------------------------------------------- | | `rhombus-cli-validate` | PreToolUse (Bash) | Validates `rhombus` commands (POST-only, kebab-case flags, JSON input) | | `rhombus-cli-update` | SessionStart | Installs the CLI if missing and checks for updates once per day | ### Example Usage ```text theme={null} # Summarize recent alerts /rhombus-alerts last 24 hours # Check deployment health /rhombus-status # The agents work automatically — Claude picks the right one: "Show me all offline cameras" → rhombus-devices agent "What alerts fired in the last hour?" → rhombus-alerts agent "What happened in the lobby at 11:30pm?" → rhombus-footage-investigator agent ``` For MSP and reseller partners managing multiple client organizations. Adds multi-org tooling on top of the CLI auto-installer. ### Commands | Command | Description | | ------------------------- | --------------------------------------------------------------------------------------------------------------- | | `/rhombus-clients` | List managed client organizations with active/inactive state and per-client camera counts | | `/rhombus-client-switch` | Set the active client organization for this workspace | | `/rhombus-audit-clients` | Metric-per-client audit across every managed org (offline cameras, alert volume, storage, license, door issues) | | `/rhombus-client-alerts` | Recent alerts scoped to a specific client organization | | `/rhombus-client-devices` | Device inventory for one client organization — cameras, doors, and sensors | | `/rhombus-fleet-report` | Cross-client fleet report — weekly health, monthly executive, or incident rollup | ### Skills | Skill | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------- | | `rhombus-partner-cli` | Partner CLI reference — authentication, the `--partner-org` flag, and multi-org workflows | | `rhombus-cross-client-reporting` | Fleet-wide health and activity reports across every managed organization | | `rhombus-partner-onboarding` | Runbook for onboarding a new client org — API access, admin invite, retention, alert routing, smoke test | ### Agents | Agent | Description | | --------------------------- | ----------------------------------------------------------------------------- | | `rhombus-client-selector` | Resolves ambiguous client names to the canonical org name or UUID | | `rhombus-fleet-ops` | Runs operations across every managed org — audits, health checks, and rollups | | `rhombus-client-onboarding` | Walks through adding a new client to partner management, validating each step | ### Hooks | Hook | Trigger | Description | | ------------------------------ | ----------------- | --------------------------------------------------------------- | | `rhombus-cli-validate` | PreToolUse (Bash) | Partner-aware `rhombus` command validation | | `rhombus-partner-session-init` | SessionStart | Loads the active-client session state for partner workflows | | `rhombus-cli-update` | SessionStart | Installs the CLI if missing and checks for updates once per day | ### Example Usage ```text theme={null} # List managed client orgs /rhombus-clients # Set the active client, then work against it /rhombus-client-switch "Acme Corp" # Audit offline cameras across the whole fleet /rhombus-audit-clients offline-cameras ``` ## Prerequisites * [Claude Code](https://claude.ai/code) installed (CLI, desktop app, or IDE extension) * For user/partner plugins: [Rhombus CLI](/rhombus-cli) installed and authenticated ## Manual Installation If you prefer to embed skills directly in your project instead of using the plugin marketplace: ```bash theme={null} git clone https://github.com/RhombusSystems/claude-code-plugins ``` ```bash theme={null} cp -r claude-code-plugins/plugins/developer/skills/code-review ./skills/ ``` The skill is available in Claude Code from your project directory. ## How Plugins Work Each plugin contains a combination of: * **Commands** — Slash commands (e.g. `/rhombus-find-endpoint`) that run a specific, often side-effecting, task on demand. * **Skills** — Markdown files (`SKILL.md`) with instructions Claude follows when a task matches. Skills can include supporting files like agent prompts, scripts, and templates. * **Agents** — Specialized sub-agent prompts that Claude dispatches for specific tasks (device management, alert investigation, etc.) * **Hooks** — Automatic actions triggered by Claude Code events (e.g., validating commands before execution, checking for CLI updates on session start) ## Resources Source code, all plugins, and contributing guide Required for user and partner plugins Alternative: connect Claude to Rhombus via MCP server Search Rhombus docs from any MCP-compatible AI tool # Documentation MCP Source: https://api-docs.rhombus.community/documentation-mcp Connect Claude, Cursor, VS Code, and other AI tools directly to Rhombus API documentation using the Model Context Protocol (MCP) server. The **Model Context Protocol (MCP)** is an open standard that creates standardized connections between AI applications and external services. Rhombus provides an MCP server that gives AI tools like Claude Code, Cursor, and Claude Desktop instant access to our complete API documentation. **Quick Access** Rhombus Developer Documentation MCP Server: `https://api-docs.rhombus.community/mcp` ## Why Use the Documentation MCP? AI assistants can search and retrieve relevant documentation without leaving your workflow Automatically synced with the latest Rhombus API documentation AI tools understand your API and provide accurate, documentation-backed responses Get answers about endpoints, parameters, and examples instantly while coding ## What's Included The Rhombus Documentation MCP provides: * **Search Tool**: Query the complete Rhombus API documentation * **Endpoint Information**: Details about all available API endpoints * **Code Examples**: Access to implementation examples and best practices * **Authentication Guide**: Information about API keys and authentication methods * **Integration Patterns**: Video player implementations and advanced use cases ## Setup Instructions Choose your AI tool below for setup instructions: ### Add MCP to Claude Code Claude Code makes it simple to add MCP servers with a single command. Open your terminal or command prompt Execute the following command: ```bash theme={null} claude mcp add --transport http rhombus-docs https://api-docs.rhombus.community/mcp ``` This adds the Rhombus documentation MCP with the name `rhombus-docs`. The MCP will be immediately available in Claude Code. You can verify by asking: ```text theme={null} "Search the Rhombus documentation for camera endpoints" ``` Claude Code will now use the MCP to retrieve accurate documentation. ### Using the MCP in Claude Code Once configured, Claude Code will automatically: * Search documentation when you ask API-related questions * Reference specific endpoints when building integrations * Provide code examples from the documentation * Suggest best practices based on official guides **Example queries:** * "How do I authenticate with the Rhombus API?" * "Show me camera endpoints for retrieving video" * "What parameters does the access control API accept?" * "How do I implement the video player?" ### Add MCP to Claude Desktop Connect Claude Desktop to Rhombus documentation for instant API reference. Click on your profile icon in Claude Desktop and select **Settings** Go to the **Connectors** section in settings Click **Add custom connector** and fill in: * **Name**: Rhombus Documentation * **URL**: `https://api-docs.rhombus.community/mcp` * **Type**: Model Context Protocol (MCP) Save the connector and test it by asking: ```text theme={null} "What camera APIs does Rhombus provide?" ``` Claude will now access the documentation to answer your questions. The connector will appear with a small icon next to responses that used the documentation MCP. ### Add MCP to Cursor Integrate Rhombus documentation into Cursor for AI-powered coding assistance. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) Type "Open MCP settings" and select it from the list In the MCP settings file, add the Rhombus documentation server: ```json theme={null} { "mcpServers": { "rhombus-docs": { "url": "https://api-docs.rhombus.community/mcp", "transport": "http" } } } ``` Restart Cursor to load the new MCP configuration Use Cursor's AI features and ask: ```text theme={null} "Search Rhombus docs for authentication methods" ``` ### Using in Cursor Composer When using Cursor Composer or Chat, the MCP enables: * Automatic documentation lookup during coding * Inline suggestions based on API documentation * Code generation using official examples * Real-time validation against API specifications ### Add MCP to VS Code Configure the Rhombus MCP for use with VS Code AI extensions. Create a `.vscode/mcp.json` file in your workspace root: ```bash theme={null} mkdir -p .vscode touch .vscode/mcp.json ``` Add the following to `.vscode/mcp.json`: ```json theme={null} { "mcpServers": { "rhombus-docs": { "url": "https://api-docs.rhombus.community/mcp", "transport": "http", "description": "Rhombus API Documentation" } } } ``` Install an AI extension that supports MCP, such as: * Continue * Cody * Other MCP-compatible VS Code extensions The MCP will be available to your AI assistant. Ask questions like: ```text theme={null} "How do I get a list of cameras from Rhombus API?" ``` ### Generic MCP Configuration Most MCP-compatible tools follow a similar pattern: **Connection Details:** * **Server URL**: `https://api-docs.rhombus.community/mcp` * **Transport**: HTTP * **Name**: rhombus-docs (or your preference) **Configuration Example (JSON):** ```json theme={null} { "name": "rhombus-docs", "url": "https://api-docs.rhombus.community/mcp", "transport": "http" } ``` **Supported Tools:** * Goose AI * Any MCP-compatible AI assistant * Custom integrations using MCP SDK Check your tool's documentation for specific MCP configuration instructions. ## Available Tools The Rhombus Documentation MCP provides the following tools to AI assistants: **Search the complete Rhombus documentation** This tool allows AI assistants to search through all documentation pages, including: * API endpoint references * Authentication guides * Implementation examples * Video player documentation * Best practices and tutorials **Example Usage:** ```text theme={null} Tool: search_rhombus_developer Query: "camera video streaming endpoints" ``` **Results:** Returns relevant documentation snippets with links to full pages. Pass an optional `language` code (for example `es`) to search a translated version. **Read documentation pages and OpenAPI specs directly** Runs read-only, shell-like commands against an in-memory filesystem that contains every documentation page (as `.mdx`) and the OpenAPI specs: * `ls` / `tree` to explore the documentation structure * `cat` / `head` to read a full page * `rg` for exact keyword or regex matches This is how AI assistants read a complete page — there is no separate "get page" tool. **Example Usage:** ```text theme={null} Tool: query_docs_filesystem_rhombus_developer Command: head -200 /api-reference/overview.mdx ``` **Results:** Returns the raw page content or search matches from the documentation filesystem. **Report a documentation problem** Lets AI assistants flag a page that is incorrect, outdated, confusing, or incomplete so the Rhombus docs team can review it. Use it when you spot an error while browsing the documentation. ## Example Workflows ### Building a Video Integration ```text theme={null} "What endpoints do I need to stream video from a Rhombus camera?" ``` The MCP will search documentation and provide relevant endpoints with descriptions. ```text theme={null} "Show me how to implement the DashJS video player" ``` Retrieves the complete video player implementation guide with code examples. ```text theme={null} "How do I authenticate for video streaming?" ``` Provides federated token authentication details and code samples. ```text theme={null} "Give me a complete JavaScript example for streaming video" ``` Returns working code examples from the documentation. ### Exploring Access Control ```text theme={null} "List all access control endpoints in the Rhombus API" ``` MCP searches and returns comprehensive list of access control endpoints. ```text theme={null} "What parameters does createAccessGrant accept?" ``` Retrieves detailed parameter documentation for the specific endpoint. ```text theme={null} "Show me an example of creating an access grant" ``` Provides code examples and usage patterns from documentation. ## Best Practices Ask targeted questions about specific endpoints or features for better results Mention "Rhombus API" or specific endpoint names to get focused documentation Explicitly ask for code examples when you need implementation help Ask follow-up questions to drill deeper into specific topics ## Troubleshooting **Possible causes:** * MCP server URL is incorrect * Network connectivity issues * Tool needs to be restarted **Solutions:** 1. Verify the URL: `https://api-docs.rhombus.community/mcp` 2. Check your internet connection 3. Restart your AI tool 4. Re-add the MCP configuration **Try these approaches:** * Rephrase your question more specifically * Use exact endpoint names or feature names * Break complex questions into simpler queries * Check if you're asking about a feature that exists in Rhombus API The MCP server automatically syncs with the latest documentation. If you suspect outdated information: * The documentation may have been recently updated * Try rephrasing to get different results * Verify against the online documentation **For command-line tools:** ```bash theme={null} # Remove and re-add the MCP claude mcp remove rhombus-docs claude mcp add --transport http rhombus-docs https://api-docs.rhombus.community/mcp ``` **For GUI tools:** * Remove the MCP configuration * Restart the application * Re-add the configuration * Verify the URL is exactly: `https://api-docs.rhombus.community/mcp` ## Privacy & Security **Data Privacy** The MCP server only provides access to public Rhombus API documentation. No private data, API keys, or user information is transmitted through the MCP. **What the MCP provides:** * Public API documentation * Code examples * Integration guides * Best practices **What the MCP does NOT provide:** * Your API keys or credentials * Customer-specific data * Private account information * Unreleased features or internal documentation ## Additional Resources Learn more about the MCP standard Implementation examples and tutorials Get help and share experiences ## Feedback Have suggestions for improving the Documentation MCP? * **Community**: Share feedback at [rhombus.community](https://rhombus.community) * **Email**: Reach out to [support@rhombus.com](mailto:support@rhombus.com) The Documentation MCP is continuously improved based on developer feedback and usage patterns. Check back for new features and capabilities. # Access Control Source: https://api-docs.rhombus.community/implementations/access-control Manage Rhombus access control through the API — list access-controlled doors, remotely unlock, issue and assign credentials, build access groups and grants, and activate lockdown plans. ## Overview Rhombus access control lets you manage physical entry across your facilities from a single API. Access-controlled doors are backed by Rhombus door controllers and readers, and every unlock, credential, and grant is tied to the same platform that records your camera footage — so each entry event has visual context. Through the API, you can: * **List access-controlled doors** and inspect their configuration and live state * **Remotely unlock** a door for a momentary entry * **Issue credentials** (such as Wiegand cards) and assign them to users * **Build access groups and grants** that tie users, doors, and schedules together * **Activate lockdown plans** to secure a location during an emergency Looking for touchless visitor entry? The [QR Code Access Control](/implementations/qr-code-access-control) guide (currently in beta) covers generating time-bound QR codes that a Rhombus camera reads to unlock a door. This guide covers the broader, generally available access control surface. ## Prerequisites Before you begin, make sure you have: * A **Rhombus API key** with access control permissions (generated in the Rhombus Console under Settings > API) * At least one **access-controlled door** configured with a Rhombus door controller and reader * The **UUIDs** of the users, doors, and locations you plan to work with (you can discover door UUIDs with the door listing endpoint below) All requests are `POST`, target the base URL `https://api2.rhombussystems.com`, and require these headers: ```bash theme={null} x-auth-scheme: api-token x-auth-apikey: YOUR_API_KEY Content-Type: application/json ``` ## List access-controlled doors Retrieve the access-controlled doors in your organization. The response is paginated: pass an empty body for the first page, then send the returned `lastEvaluatedKey` on subsequent requests until it comes back empty. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/component/findAccessControlledDoors", headers=headers, json={ "maxPageSize": 100 } ) data = response.json() for door in data.get("accessControlledDoors", []): print(f"{door['name']} ({door['uuid']}) - default state: {door.get('defaultState')}") # Paginate if more doors remain next_key = data.get("lastEvaluatedKey") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/component/findAccessControlledDoors', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ maxPageSize: 100 }) }); const data = await response.json(); for (const door of data.accessControlledDoors || []) { console.log(`${door.name} (${door.uuid}) - default state: ${door.defaultState}`); } const nextKey = data.lastEvaluatedKey; ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/component/findAccessControlledDoors \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"maxPageSize": 100}' ``` ### Request parameters Maximum number of doors to return in a single page. Pagination cursor returned by a previous call. Omit it on the first request; supply it to fetch the next page. ### Response The list of access-controlled doors. Key fields per door: Unique identifier for the door. Use this value wherever an `accessControlledDoorUuid` is required. Display name of the door (for example, "Main Entrance"). The location the door belongs to. Baseline access mode, one of `ACCESS_CONTROLLED` or `UNLOCKED`. Whether the door accepts remote unlock requests through the API. How long the door stays unlocked after a standard unlock, in seconds. Pagination cursor. When present, pass it back in the next request to retrieve additional doors. If you only need lightweight door status rather than full configuration, use `/api/component/findMinimalStateAccessControlledDoors`, which returns each door plus a compact state shadow. ## Remotely unlock a door Trigger a momentary unlock on an access-controlled door. The door relocks automatically after its configured unlock time. Remote unlock only works when `remoteUnlockEnabled` is `true` for the target door. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/unlockAccessControlledDoor", headers=headers, json={ "accessControlledDoorUuid": "YOUR_DOOR_UUID" } ) result = response.json() print(result.get("type")) # SUCCESS or ERROR ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/accesscontrol/unlockAccessControlledDoor', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ accessControlledDoorUuid: 'YOUR_DOOR_UUID' }) }); const result = await response.json(); console.log(result.type); // SUCCESS or ERROR ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/accesscontrol/unlockAccessControlledDoor \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"accessControlledDoorUuid": "YOUR_DOOR_UUID"}' ``` The UUID of the door to unlock, from the door listing endpoint. Outcome of the request, either `SUCCESS` or `ERROR`. Every remote unlock is recorded as an access event with footage from the door's associated cameras, giving you a complete audit trail. ## Issue and assign a credential Credentials are the physical cards or fobs that a reader accepts. This example creates a Wiegand credential and assigns it to a user. Rhombus supports several credential formats — the `createWiegandCredential` endpoint takes a `wiegandFormat` and the fields relevant to that format. ### Create a Wiegand credential ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_sec = int(time.time()) one_year_sec = 365 * 24 * 60 * 60 response = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/createWiegandCredential", headers=headers, json={ "wiegandFormat": "H10301", "facilityCode": 42, "cardNumber": 10537, "userUuid": "YOUR_USER_UUID", "startDateEpochSecInclusive": now_sec, "endDateEpochSecExclusive": now_sec + one_year_sec } ) credential = response.json().get("credential", {}) print(f"Created credential {credential.get('uuid')} - status {credential.get('workflowStatus')}") ``` ```javascript JavaScript theme={null} const nowSec = Math.floor(Date.now() / 1000); const oneYearSec = 365 * 24 * 60 * 60; const response = await fetch('https://api2.rhombussystems.com/api/accesscontrol/createWiegandCredential', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ wiegandFormat: 'H10301', facilityCode: 42, cardNumber: 10537, userUuid: 'YOUR_USER_UUID', startDateEpochSecInclusive: nowSec, endDateEpochSecExclusive: nowSec + oneYearSec }) }); const { credential } = await response.json(); console.log(`Created credential ${credential.uuid} - status ${credential.workflowStatus}`); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/accesscontrol/createWiegandCredential \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "wiegandFormat": "H10301", "facilityCode": 42, "cardNumber": 10537, "userUuid": "YOUR_USER_UUID", "startDateEpochSecInclusive": 1712620800, "endDateEpochSecExclusive": 1744156800 }' ``` The Wiegand card format. One of `H10301`, `D10202`, `H10304`, `HID_CORP1000_STD_35`, `HID_CORP1000_STD_48`, `WIEGAND_64BIT_RAW`, or `CUSTOM`. The fields you populate depend on the format — for the standard 26-bit `H10301` format, supply `facilityCode` and `cardNumber`. Facility (site) code encoded on the card. Used by formats such as `H10301`. Card number encoded on the card. Site code, used by formats that encode one separately from the facility code. Company ID, used by HID Corporate 1000 formats. Raw credential value. Used by the `WIEGAND_64BIT_RAW` and `CUSTOM` formats. The user the credential belongs to. When set, the credential is created already assigned to that user. Start of the credential's validity window, in epoch **seconds** (inclusive). End of the credential's validity window, in epoch **seconds** (exclusive). Credential validity dates are expressed in epoch **seconds**, not milliseconds. Most other Rhombus endpoints use epoch milliseconds, so convert carefully. The response returns the created `credential` object, including its `uuid`, `lowercaseHexValue`, and `workflowStatus` (`ACTIVE`, `UNASSIGNED`, `SUSPENDED`, or `REVOKED`). ### Assign an existing credential to a user If a credential already exists (for example, it was created unassigned), assign it to a user by its hex value. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/assignAccessControlCredential", headers=headers, json={ "credentialHexValue": "002a2929", "userUuid": "YOUR_USER_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/accesscontrol/assignAccessControlCredential', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ credentialHexValue: '002a2929', userUuid: 'YOUR_USER_UUID' }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/accesscontrol/assignAccessControlCredential \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "credentialHexValue": "002a2929", "userUuid": "YOUR_USER_UUID" }' ``` The hex value of the credential to assign. The user to assign the credential to. Manage a credential's lifecycle with `/api/accesscontrol/suspendAccessControlCredential`, `/api/accesscontrol/unsuspendAccessControlCredential`, and `/api/accesscontrol/revokeAccessControlCredential`. To review a user's credentials, call `/api/accesscontrol/findAccessControlCredentialByUser` with their `userUuid`. ## Grant access with groups and schedules Rhombus models "who can open which doors, and when" as an **access grant**. A grant links a set of users (directly or through **access groups**) to a set of doors, optionally constrained by a **schedule**. The typical flow is: create a group, add users, then create a grant that ties the group to doors and a schedule. ### Create an access group and add users ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } # Create the group create = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/createAccessControlGroup", headers=headers, json={ "name": "Warehouse Staff", "description": "Employees allowed into the warehouse" } ) group_uuid = create.json()["group"]["uuid"] # Add users to it requests.post( "https://api2.rhombussystems.com/api/accesscontrol/addUsersToAccessControlGroup", headers=headers, json={ "groupUuid": group_uuid, "userUuids": ["USER_UUID_1", "USER_UUID_2"] } ) ``` ```javascript JavaScript theme={null} const headers = { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }; // Create the group const create = await fetch('https://api2.rhombussystems.com/api/accesscontrol/createAccessControlGroup', { method: 'POST', headers, body: JSON.stringify({ name: 'Warehouse Staff', description: 'Employees allowed into the warehouse' }) }); const groupUuid = (await create.json()).group.uuid; // Add users to it await fetch('https://api2.rhombussystems.com/api/accesscontrol/addUsersToAccessControlGroup', { method: 'POST', headers, body: JSON.stringify({ groupUuid, userUuids: ['USER_UUID_1', 'USER_UUID_2'] }) }); ``` ```bash cURL theme={null} # 1. Create the group curl -X POST https://api2.rhombussystems.com/api/accesscontrol/createAccessControlGroup \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"name": "Warehouse Staff", "description": "Employees allowed into the warehouse"}' # 2. Add users (use the group uuid from the response above) curl -X POST https://api2.rhombussystems.com/api/accesscontrol/addUsersToAccessControlGroup \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"groupUuid": "GROUP_UUID", "userUuids": ["USER_UUID_1", "USER_UUID_2"]}' ``` Name of the access group. Optional description of the group. Optional list of user UUIDs to seed the group with at creation time. You can also add members later with `addUsersToAccessControlGroup`. ### (Optional) Create a weekly schedule To restrict a grant to specific hours, first create a schedule and reference its UUID in the grant. Weekly schedules use the `WEEKLY_REPEATING_MINUTES` strategy. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/schedule/createWeeklySchedule", headers=headers, json={ "schedule": { "name": "Business Hours", "strategy": "WEEKLY_REPEATING_MINUTES" } } ) schedule_uuid = response.json().get("scheduleUuid") print(schedule_uuid) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/schedule/createWeeklySchedule', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ schedule: { name: 'Business Hours', strategy: 'WEEKLY_REPEATING_MINUTES' } }) }); const { scheduleUuid } = await response.json(); console.log(scheduleUuid); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/schedule/createWeeklySchedule \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"schedule": {"name": "Business Hours", "strategy": "WEEKLY_REPEATING_MINUTES"}}' ``` The response returns a `scheduleUuid`. Use `/api/schedule/getSchedules` and `/api/schedule/getScheduleDataV2` to review schedules, and manage the detailed weekly time intervals in the Rhombus Console. ### Create the access grant Tie the group, doors, and (optionally) a schedule together. Use mode `DEFAULT` for a standard access grant. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/createAccessGrant", headers=headers, json={ "accessGrant": { "name": "Warehouse Staff - Business Hours", "locationUuid": "YOUR_LOCATION_UUID", "mode": "DEFAULT", "groupUuids": ["GROUP_UUID"], "accessControlledDoorUuids": ["DOOR_UUID_1", "DOOR_UUID_2"], "scheduleUuid": "SCHEDULE_UUID" } } ) data = response.json() if data.get("error"): print("Error:", data.get("errorMsg")) else: print("Created grant:", data["accessGrant"]["uuid"]) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/accesscontrol/createAccessGrant', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ accessGrant: { name: 'Warehouse Staff - Business Hours', locationUuid: 'YOUR_LOCATION_UUID', mode: 'DEFAULT', groupUuids: ['GROUP_UUID'], accessControlledDoorUuids: ['DOOR_UUID_1', 'DOOR_UUID_2'], scheduleUuid: 'SCHEDULE_UUID' } }) }); const data = await response.json(); if (data.error) { console.error('Error:', data.errorMsg); } else { console.log('Created grant:', data.accessGrant.uuid); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/accesscontrol/createAccessGrant \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "accessGrant": { "name": "Warehouse Staff - Business Hours", "locationUuid": "YOUR_LOCATION_UUID", "mode": "DEFAULT", "groupUuids": ["GROUP_UUID"], "accessControlledDoorUuids": ["DOOR_UUID_1", "DOOR_UUID_2"], "scheduleUuid": "SCHEDULE_UUID" } }' ``` Human-readable name for the grant. The location the grant applies to. Grant mode, one of `DEFAULT` or `GUEST_PASS_INDIVIDUAL`. Use `DEFAULT` for standard employee access. Access groups whose members receive access. Combine with `userUuids` to grant individual users directly. Individual users to grant access to, in addition to any groups. The doors this grant opens. You can also target doors by label with `doorLabelIds`. Optional schedule that constrains when the grant is active. Omit for 24/7 access. The created grant, including its generated `uuid`. `true` if the grant could not be created. Check `errorMsg` for details and `warningMsg` for non-fatal notes. ## Activate a lockdown plan Lockdown plans let you instantly change the access state of many doors at a location during an emergency. First list the plans configured for your organization, then activate one or more for a location. ### Find available lockdown plans ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/lockdownPlan/findLockdownPlans", headers=headers, json={} ) for plan in response.json().get("lockdownPlans", []): print(f"{plan.get('name')}: {plan.get('uuid')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/accesscontrol/lockdownPlan/findLockdownPlans', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); for (const plan of (await response.json()).lockdownPlans || []) { console.log(`${plan.name}: ${plan.uuid}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/accesscontrol/lockdownPlan/findLockdownPlans \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` ### Activate lockdown for a location ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/accesscontrol/lockdownPlan/activateLockdownForLocation", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID", "lockdownPlanUuids": ["LOCKDOWN_PLAN_UUID"], "stateUpdatedAtMillis": int(time.time() * 1000) } ) data = response.json() print("Result:", data.get("result")) print("Location state:", data.get("state", {}).get("state")) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/accesscontrol/lockdownPlan/activateLockdownForLocation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID', lockdownPlanUuids: ['LOCKDOWN_PLAN_UUID'], stateUpdatedAtMillis: Date.now() }) }); const data = await response.json(); console.log('Result:', data.result); console.log('Location state:', data.state?.state); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/accesscontrol/lockdownPlan/activateLockdownForLocation \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "locationUuid": "YOUR_LOCATION_UUID", "lockdownPlanUuids": ["LOCKDOWN_PLAN_UUID"], "stateUpdatedAtMillis": 1712707200000 }' ``` The location to place into lockdown. The lockdown plans to activate. Use UUIDs from `findLockdownPlans`. The current lockdown state timestamp in epoch milliseconds, used for optimistic concurrency. Pass the current time when initiating a lockdown. Outcome of the activation, one of `SUCCESS`, `INVALID_LOCKDOWN_PLANS`, or `OPTIMISTIC_CONCURRENCY`. The location's resulting lockdown state, including `state` (`STANDARD_SECURITY` or `LOCKED_DOWN`) and the list of `activeLockdownPlans`. Activating a lockdown changes physical access at the location immediately. Test your integration with `/api/accesscontrol/lockdownPlan/enableLockdownTestModeForLocation` before using it in production, and end a lockdown with `/api/accesscontrol/lockdownPlan/deactivateLockdownForLocation`. ## Use cases Let front-desk software unlock a door on demand when a visitor is verified, with camera footage attached to every unlock. Provision a new hire's badge and group membership from your HR system so their access is ready on day one. Issue credentials with explicit start and end dates, and constrain access to business hours with a schedule. Wire a panic button or safety system to activate a lockdown plan across a location in a single API call. ## Troubleshooting Confirm the door has `remoteUnlockEnabled` set to `true` in the door listing response, that the `accessControlledDoorUuid` is correct, and that your API key has access control permissions. Doors that are offline cannot be unlocked remotely. Creating and assigning a credential is not enough on its own — the user must also fall under an access grant that covers the target door. Verify a `DEFAULT` grant links the user (directly or via a group) to the door, and that any attached schedule is currently active. Also confirm the credential's `workflowStatus` is `ACTIVE` and the current time falls within its start/end window. `startDateEpochSecInclusive` and `endDateEpochSecExclusive` use epoch **seconds**, not milliseconds. If a credential expires immediately or never activates, check that you did not pass a millisecond value. The location's lockdown state changed between your read and your write. Re-fetch the current state (for example, with `getOrCreateLocationLockdownState`), then retry the activation with an updated `stateUpdatedAtMillis`. Inspect `errorMsg` in the response. Common causes are referencing a door that has no available access control license (see `unassignedACDLicensesDoorUuids` and `expiredACDLicensesDoorUuids` in the response) or an invalid `locationUuid`. ## Next steps Add touchless, camera-authenticated QR code entry for visitors and events (beta). Receive real-time access events so your application can react the moment a door is used. Explore complete request and response schemas for every access control endpoint. # Multi-Camera Video Synchronization with ISOBMFF Source: https://api-docs.rhombus.community/implementations/advanced-implementation Extract Rhombus millisecond timestamps embedded in ISOBMFF video containers to align frames precisely across multiple cameras for forensic analysis. When dealing with video segments—especially in systems that rely on distributed cameras, cloud storage, and multiple data streams—**time synchronization** becomes one of the most critical challenges. Without precise timing information, aligning events between multiple feeds (e.g., video, audio, and sensor data) becomes error-prone. Rhombus has implemented a **custom timestamp embedding** strategy that provides millisecond precision within `.mp4`/`.m4v` video segments, going beyond the coarse timing fields traditionally found in standard ISOBMFF (ISO Base Media File Format) containers. This guide explains what this approach means, why it's important, and how developers can retrieve and use this timestamp for building time-aligned applications. ## Understanding ISOBMFF and the "free" Atom The **ISOBMFF** standard (ISO/IEC 14496-12) is the container format underlying `.mp4`, `.m4v`, `.mov`, and many streaming segment formats like `.fMP4`. Its structure is based on **boxes (atoms)**—self-contained data units identified by a 4-character code (e.g., `moov`, `mdat`, `free`). **Standard timestamps** in ISOBMFF (e.g., `creation_time` in the `mvhd` box) typically have **seconds-level resolution**. This is fine for some media workflows, but insufficient for **multi-camera synchronization** or **high-speed event correlation**. Rhombus solves this by **embedding a millisecond-precision timestamp** inside a `free` atom. This is a non-standard, yet fully ISOBMFF-compliant, method. ## The Rhombus Custom Timestamp When Rhombus segments video (and if pulling actual segments, not a live transport stream), a **custom metadata signature** is written into the `free` box: In ISOBMFF, `free` is normally a placeholder box containing unused space. Rhombus repurposes this to carry timestamp metadata. The first 4 bytes are the ASCII string `rhom`—identifying it as **Rhombus-specific data**. The next **8 bytes** are a **64-bit integer** representing the start time of the video content, measured in **milliseconds since the Unix epoch** (UTC). **Binary layout example:** ```text theme={null} [ free box length ][ 'free' ][ 'rhom' ][ 8-byte timestamp (ms since epoch) ] ``` ## Why This Is Important for Developers This design choice unlocks **precise synchronization** capabilities across multiple use cases: Align video feeds from different cameras to within 1 ms for coordinated monitoring Merge video with IoT sensor data (access control events, environmental readings) Reconstruct events down to sub-second intervals in investigations Avoid errors that accumulate when relying solely on client system clocks or NTP sync For ecosystem and integration partners, this makes Rhombus video streams **highly interoperable** with third-party analytics, AI/ML pipelines, and real-time monitoring systems. ## Retrieving the Timestamp ### Parsing ISOBMFF Files You can use open-source libraries to read the `free` box from an `.mp4`/`.m4v` segment and check for the `rhom` signature. Professional library for parsing ISO Base Media File Format Online tool for visually inspecting MP4 box structures ## Implementation Examples ```python Python Implementation theme={null} import struct import datetime def extract_rhombus_timestamp(file_path): """ Extract millisecond-precision timestamp from Rhombus video segment. Args: file_path: Path to the .mp4 or .m4v file Returns: tuple: (timestamp_ms, datetime object) or (None, None) if not found """ with open(file_path, "rb") as f: data = f.read() # Find the 'free' box idx = data.find(b'free') if idx == -1: return None, None # Search for 'rhom' signature after 'free' rhom_idx = data.find(b'rhom', idx) if rhom_idx == -1: return None, None # Read the next 8 bytes after 'rhom' (big-endian 64-bit integer) timestamp_bytes = data[rhom_idx + 4 : rhom_idx + 12] timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big") # Convert to human-readable UTC time timestamp_dt = datetime.datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc) return timestamp_ms, timestamp_dt # Example usage: timestamp_ms, timestamp_dt = extract_rhombus_timestamp("video_segment.mp4") if timestamp_ms: print(f"Timestamp (ms since epoch): {timestamp_ms}") print(f"UTC Time: {timestamp_dt}") print(f"ISO Format: {timestamp_dt.isoformat()}") else: print("Rhombus timestamp not found in file") # Sample output: # Timestamp (ms since epoch): 1722945678123 # UTC Time: 2024-08-06 15:21:18.123000 # ISO Format: 2024-08-06T15:21:18.123000 ``` ### Advanced Python Usage ```python Multi-Segment Processing theme={null} import os import glob from datetime import datetime class RhombusTimestampExtractor: """Extract and manage timestamps from multiple video segments.""" def __init__(self): self.timestamps = [] def process_directory(self, directory_path, pattern="*.mp4"): """Process all video files in a directory.""" files = glob.glob(os.path.join(directory_path, pattern)) for file_path in sorted(files): timestamp_ms, timestamp_dt = extract_rhombus_timestamp(file_path) if timestamp_ms: self.timestamps.append({ 'file': os.path.basename(file_path), 'timestamp_ms': timestamp_ms, 'datetime': timestamp_dt }) def get_timeline(self): """Get chronologically sorted list of segments.""" return sorted(self.timestamps, key=lambda x: x['timestamp_ms']) def find_segment_at_time(self, target_datetime): """Find the video segment containing a specific time.""" target_ms = int(target_datetime.timestamp() * 1000) for segment in self.get_timeline(): if segment['timestamp_ms'] <= target_ms: closest = segment else: break return closest # Usage example extractor = RhombusTimestampExtractor() extractor.process_directory("/path/to/video/segments") # Find segment at specific time target = datetime(2024, 8, 6, 15, 21, 0) segment = extractor.find_segment_at_time(target) print(f"Segment at {target}: {segment['file']}") ``` ```javascript JavaScript Implementation theme={null} /** * Extract Rhombus timestamp from video segment ArrayBuffer * @param {ArrayBuffer} arrayBuffer - The video file as ArrayBuffer * @returns {Object|null} Object with timestampMs and utcTime, or null if not found */ function extractRhombusTimestamp(arrayBuffer) { const data = new Uint8Array(arrayBuffer); // Find 'free' box const freePattern = new Uint8Array([0x66, 0x72, 0x65, 0x65]); // 'free' let freeIndex = -1; for (let i = 0; i <= data.length - 4; i++) { if (data.subarray(i, i + 4).every((val, idx) => val === freePattern[idx])) { freeIndex = i; break; } } if (freeIndex === -1) return null; // Find 'rhom' signature const rhomPattern = new Uint8Array([0x72, 0x68, 0x6f, 0x6d]); // 'rhom' let rhomIndex = -1; for (let i = freeIndex; i <= data.length - 4; i++) { if (data.subarray(i, i + 4).every((val, idx) => val === rhomPattern[idx])) { rhomIndex = i; break; } } if (rhomIndex === -1) return null; // Extract 8-byte timestamp (big-endian) const timestampBytes = data.subarray(rhomIndex + 4, rhomIndex + 12); let timestamp = 0; for (let i = 0; i < 8; i++) { timestamp = timestamp * 256 + timestampBytes[i]; } return { timestampMs: timestamp, utcTime: new Date(timestamp), isoString: new Date(timestamp).toISOString() }; } // Example usage with File API async function processVideoFile(file) { const arrayBuffer = await file.arrayBuffer(); const result = extractRhombusTimestamp(arrayBuffer); if (result) { console.log('Timestamp (ms):', result.timestampMs); console.log('UTC Time:', result.utcTime); console.log('ISO String:', result.isoString); } else { console.log('Rhombus timestamp not found'); } } // Usage with input element document.querySelector('#fileInput').addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) { await processVideoFile(file); } }); ``` ### Advanced JavaScript Usage ```javascript Multi-Camera Synchronization theme={null} class RhombusVideoSync { constructor() { this.cameras = new Map(); } /** * Add a camera's video segment with its timestamp */ async addCameraSegment(cameraId, videoFile) { const arrayBuffer = await videoFile.arrayBuffer(); const timestamp = extractRhombusTimestamp(arrayBuffer); if (timestamp) { if (!this.cameras.has(cameraId)) { this.cameras.set(cameraId, []); } this.cameras.get(cameraId).push({ timestamp: timestamp.timestampMs, datetime: timestamp.utcTime, file: videoFile }); } } /** * Find segments from all cameras at a specific time */ findSegmentsAtTime(targetTime) { const targetMs = targetTime.getTime(); const results = {}; for (const [cameraId, segments] of this.cameras.entries()) { // Find closest segment before or at target time const sorted = segments.sort((a, b) => a.timestamp - b.timestamp); const segment = sorted.find(s => s.timestamp <= targetMs); if (segment) { results[cameraId] = { timestamp: segment.timestamp, datetime: segment.datetime, offset: targetMs - segment.timestamp }; } } return results; } /** * Get time range covered by all cameras */ getCommonTimeRange() { let earliestEnd = Infinity; let latestStart = 0; for (const segments of this.cameras.values()) { if (segments.length === 0) continue; const start = Math.min(...segments.map(s => s.timestamp)); const end = Math.max(...segments.map(s => s.timestamp)); latestStart = Math.max(latestStart, start); earliestEnd = Math.min(earliestEnd, end); } return { start: new Date(latestStart), end: new Date(earliestEnd), duration: earliestEnd - latestStart }; } } // Usage const sync = new RhombusVideoSync(); await sync.addCameraSegment('camera1', file1); await sync.addCameraSegment('camera2', file2); await sync.addCameraSegment('camera3', file3); // Find what was happening across all cameras at a specific time const target = new Date('2024-08-06T15:21:18.123Z'); const segments = sync.findSegmentsAtTime(target); console.log('Synchronized segments:', segments); ``` ```cpp C++ Implementation theme={null} #include #include #include #include #include #include #include struct RhombusTimestamp { uint64_t timestampMs; std::chrono::system_clock::time_point utcTime; // Convert to ISO 8601 string std::string toISO8601() const { auto timeT = std::chrono::system_clock::to_time_t(utcTime); auto ms = std::chrono::duration_cast( utcTime.time_since_epoch() ).count() % 1000; char buffer[32]; std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", std::gmtime(&timeT)); return std::string(buffer) + "." + std::to_string(ms) + "Z"; } }; /** * Extract Rhombus timestamp from video file */ std::optional extractRhombusTimestamp( const std::string& filePath ) { std::ifstream file(filePath, std::ios::binary); if (!file) { return std::nullopt; } // Read entire file into memory std::vector data( (std::istreambuf_iterator(file)), std::istreambuf_iterator() ); // Find 'free' box const std::vector freePattern = {0x66, 0x72, 0x65, 0x65}; auto freeIt = std::search( data.begin(), data.end(), freePattern.begin(), freePattern.end() ); if (freeIt == data.end()) { return std::nullopt; } // Find 'rhom' signature const std::vector rhomPattern = {0x72, 0x68, 0x6f, 0x6d}; auto rhomIt = std::search( freeIt, data.end(), rhomPattern.begin(), rhomPattern.end() ); if (rhomIt == data.end()) { return std::nullopt; } // Extract 8-byte timestamp (big-endian) uint64_t timestamp = 0; for (int i = 0; i < 8; i++) { timestamp = (timestamp << 8) | *(rhomIt + 4 + i); } // Convert to time_point auto utcTime = std::chrono::system_clock::from_time_t( timestamp / 1000 ); utcTime += std::chrono::milliseconds(timestamp % 1000); return RhombusTimestamp{timestamp, utcTime}; } // Example usage int main() { auto result = extractRhombusTimestamp("video_segment.mp4"); if (result) { std::cout << "Timestamp (ms): " << result->timestampMs << std::endl; std::cout << "ISO 8601: " << result->toISO8601() << std::endl; } else { std::cout << "Rhombus timestamp not found" << std::endl; } return 0; } ``` ### Advanced C++ Usage ```cpp Multi-Segment Timeline theme={null} #include #include class RhombusTimeline { private: struct Segment { std::filesystem::path filePath; RhombusTimestamp timestamp; }; std::vector segments; public: /** * Load all video segments from a directory */ void loadDirectory(const std::filesystem::path& directory) { for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (entry.path().extension() == ".mp4" || entry.path().extension() == ".m4v") { auto timestamp = extractRhombusTimestamp( entry.path().string() ); if (timestamp) { segments.push_back({entry.path(), *timestamp}); } } } // Sort by timestamp std::sort(segments.begin(), segments.end(), [](const Segment& a, const Segment& b) { return a.timestamp.timestampMs < b.timestamp.timestampMs; } ); } /** * Find segment containing a specific time */ std::optional findSegmentAtTime( const std::chrono::system_clock::time_point& targetTime ) const { auto targetMs = std::chrono::duration_cast< std::chrono::milliseconds >(targetTime.time_since_epoch()).count(); Segment* closest = nullptr; for (const auto& segment : segments) { if (segment.timestamp.timestampMs <= targetMs) { closest = const_cast(&segment); } else { break; } } return closest ? std::optional(*closest) : std::nullopt; } /** * Get chronological list of all segments */ const std::vector& getTimeline() const { return segments; } }; ``` ## Real-World Use Cases ### Multi-Camera Event Reconstruction Synchronize footage from multiple cameras to reconstruct security incidents: ```python Event Timeline Reconstruction theme={null} from datetime import datetime, timedelta class EventReconstructor: def __init__(self): self.camera_segments = {} # cameraId -> list of segments def add_camera_footage(self, camera_id, segment_files): """Add video segments from a specific camera.""" segments = [] for file_path in segment_files: timestamp_ms, timestamp_dt = extract_rhombus_timestamp(file_path) if timestamp_ms: segments.append({ 'file': file_path, 'start_time': timestamp_dt, 'timestamp_ms': timestamp_ms }) self.camera_segments[camera_id] = sorted( segments, key=lambda x: x['timestamp_ms'] ) def reconstruct_event(self, event_time, window_seconds=30): """ Find all camera segments within time window of an event. Args: event_time: datetime of the event window_seconds: seconds before/after event to include """ window = timedelta(seconds=window_seconds) start_time = event_time - window end_time = event_time + window relevant_footage = {} for camera_id, segments in self.camera_segments.items(): camera_clips = [] for segment in segments: # Check if segment overlaps with event window if start_time <= segment['start_time'] <= end_time: offset = (segment['start_time'] - event_time).total_seconds() camera_clips.append({ 'file': segment['file'], 'start_time': segment['start_time'], 'offset_from_event': offset }) if camera_clips: relevant_footage[camera_id] = camera_clips return relevant_footage # Usage reconstructor = EventReconstructor() reconstructor.add_camera_footage('entrance', entrance_files) reconstructor.add_camera_footage('lobby', lobby_files) reconstructor.add_camera_footage('parking', parking_files) # Reconstruct event at specific time event_time = datetime(2024, 8, 6, 15, 21, 18) footage = reconstructor.reconstruct_event(event_time, window_seconds=30) print(f"Footage for event at {event_time}:") for camera_id, clips in footage.items(): print(f"\n{camera_id}:") for clip in clips: print(f" - {clip['file']}") print(f" Offset: {clip['offset_from_event']:.2f}s") ``` ### Sensor Data Correlation Align video with access control or environmental sensor events: ```javascript Sensor-Video Correlation theme={null} class SensorVideoCorrelator { constructor() { this.videoTimestamps = []; this.sensorEvents = []; } /** * Add video segment with its timestamp */ addVideoSegment(cameraId, timestamp, videoUrl) { this.videoTimestamps.push({ cameraId, timestamp: timestamp.timestampMs, datetime: timestamp.utcTime, url: videoUrl }); } /** * Add sensor event with timestamp */ addSensorEvent(sensorId, eventType, timestampMs, data) { this.sensorEvents.push({ sensorId, eventType, timestamp: timestampMs, datetime: new Date(timestampMs), data }); } /** * Find video coverage for a sensor event */ findVideoForEvent(eventTimestampMs, cameras = null) { const relevantVideos = this.videoTimestamps.filter(video => { // Video segment starts before or at event time const inTimeRange = video.timestamp <= eventTimestampMs; // Filter by camera if specified const inCameraList = !cameras || cameras.includes(video.cameraId); return inTimeRange && inCameraList; }); // Get the most recent video before the event for each camera const latestByCamera = {}; relevantVideos.forEach(video => { if (!latestByCamera[video.cameraId] || video.timestamp > latestByCamera[video.cameraId].timestamp) { latestByCamera[video.cameraId] = video; } }); return Object.values(latestByCamera); } /** * Generate correlation report */ generateCorrelationReport() { return this.sensorEvents.map(event => { const videos = this.findVideoForEvent(event.timestamp); return { event: { type: event.eventType, sensor: event.sensorId, time: event.datetime.toISOString(), data: event.data }, associatedVideos: videos.map(v => ({ camera: v.cameraId, url: v.url, offset: event.timestamp - v.timestamp })) }; }); } } // Usage example const correlator = new SensorVideoCorrelator(); // Add door access event correlator.addSensorEvent( 'door-entrance-1', 'ACCESS_GRANTED', 1722945678123, { userId: 'user123', cardId: '12345' } ); // Add corresponding video correlator.addVideoSegment( 'camera-entrance', { timestampMs: 1722945670000, utcTime: new Date(1722945670000) }, 'https://media.rhombussystems.com/segment1.mp4' ); // Generate report const report = correlator.generateCorrelationReport(); console.log(JSON.stringify(report, null, 2)); ``` ## Best Practices for Integration Always confirm the `rhom` tag before interpreting the following bytes as a timestamp. This prevents misinterpretation of unrelated data. ```python theme={null} def is_valid_rhombus_timestamp(data, rhom_index): # Verify 'rhom' signature if data[rhom_index:rhom_index + 4] != b'rhom': return False # Verify sufficient data for timestamp if len(data) < rhom_index + 12: return False # Extract and validate timestamp range timestamp_bytes = data[rhom_index + 4:rhom_index + 12] timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big") # Sanity check: timestamp should be reasonable # (between 2015 and 2050) min_timestamp = 1420070400000 # Jan 1, 2015 max_timestamp = 2524608000000 # Jan 1, 2050 return min_timestamp <= timestamp_ms <= max_timestamp ``` The timestamp is UTC-based. Convert it appropriately if your application needs local time. ```python theme={null} import datetime as dt import pytz def convert_to_local_time(timestamp_ms, timezone='America/New_York'): # Create UTC datetime utc_dt = dt.datetime.fromtimestamp(timestamp_ms / 1000.0, tz=dt.timezone.utc) utc_dt = pytz.utc.localize(utc_dt) # Convert to local timezone local_tz = pytz.timezone(timezone) local_dt = utc_dt.astimezone(local_tz) return local_dt # Usage timestamp_ms = 1722945678123 local_time = convert_to_local_time(timestamp_ms, 'America/Los_Angeles') print(f"Local time: {local_time}") ``` Combine the segment start timestamp with frame timestamps for frame-accurate alignment. ```javascript theme={null} class FrameTimestampCalculator { constructor(segmentStartMs, frameRate) { this.segmentStartMs = segmentStartMs; this.frameRate = frameRate; this.frameDurationMs = 1000 / frameRate; } /** * Calculate absolute timestamp for a specific frame */ getFrameTimestamp(frameNumber) { const offsetMs = frameNumber * this.frameDurationMs; return this.segmentStartMs + offsetMs; } /** * Find frame number for a specific timestamp */ getFrameAtTimestamp(targetTimestampMs) { const offsetMs = targetTimestampMs - this.segmentStartMs; return Math.floor(offsetMs / this.frameDurationMs); } } // Usage const segmentTimestamp = 1722945678123; const calculator = new FrameTimestampCalculator(segmentTimestamp, 30); // Get timestamp of frame 150 const frameTime = calculator.getFrameTimestamp(150); console.log('Frame 150 timestamp:', new Date(frameTime)); // Find frame at specific time const targetTime = 1722945683123; const frameNum = calculator.getFrameAtTimestamp(targetTime); console.log('Frame at target time:', frameNum); ``` Store your parsing logic in a modular way in case Rhombus adds new metadata formats. ```python theme={null} class RhombusMetadataParser: """Extensible parser for Rhombus metadata formats.""" VERSION = "1.0" def __init__(self): self.parsers = { b'rhom': self._parse_v1_timestamp } def parse(self, file_path): """Parse metadata from video file.""" with open(file_path, "rb") as f: data = f.read() # Find 'free' box free_idx = data.find(b'free') if free_idx == -1: return None # Check all known signatures for signature, parser_func in self.parsers.items(): sig_idx = data.find(signature, free_idx) if sig_idx != -1: return parser_func(data, sig_idx) return None def _parse_v1_timestamp(self, data, rhom_idx): """Parse v1 timestamp format.""" timestamp_bytes = data[rhom_idx + 4:rhom_idx + 12] timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big") return { 'version': 1, 'type': 'timestamp', 'timestamp_ms': timestamp_ms, 'datetime': datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc) } def add_parser(self, signature, parser_func): """Add custom parser for new metadata formats.""" self.parsers[signature] = parser_func # Usage parser = RhombusMetadataParser() metadata = parser.parse("video_segment.mp4") if metadata: print(f"Version: {metadata['version']}") print(f"Type: {metadata['type']}") print(f"Timestamp: {metadata['datetime']}") ``` ## Performance Considerations For large files, read only the header portion instead of the entire file Cache extracted timestamps to avoid re-parsing the same files Process multiple files in parallel when building timelines Use streaming parsers for very large video files ### Optimized File Reading ```python Optimized Parser theme={null} def extract_rhombus_timestamp_optimized(file_path, max_search_bytes=100_000): """ Optimized version that reads only the beginning of the file. Most metadata is in the first portion of MP4 files. """ with open(file_path, "rb") as f: # Read only header portion data = f.read(max_search_bytes) # Search for 'rhom' signature rhom_idx = data.find(b'rhom') if rhom_idx == -1: return None, None # Verify we have enough data for timestamp if len(data) < rhom_idx + 12: return None, None timestamp_bytes = data[rhom_idx + 4:rhom_idx + 12] timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big") timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc) return timestamp_ms, timestamp_dt ``` ## Conclusion Rhombus' method of embedding a **`millisecond-precision UTC timestamp in the free atom`** of ISOBMFF segments provides developers with a powerful tool for **precise event alignment** in multi-stream environments. This approach preserves compatibility with existing video tooling while unlocking **sub-second accuracy** for analytics, AI, and real-time monitoring—critical for advanced integrations in the Rhombus ecosystem. **Next Steps for Developers:** * Experiment with the [ISOBMFF GitHub library](https://github.com/DigiDNA/ISOBMFF) to parse Rhombus segments * Use the [MP4Box.js online viewer](https://gpac.github.io/mp4box.js/test/filereader.html) to visually inspect box structures * Incorporate timestamp extraction into your ingest pipeline for perfectly synchronized multi-source datasets ## Additional Resources Learn how to implement live video streaming with DashJS Explore camera and media API endpoints as well as other video options Read the official ISO Base Media File Format spec Get help and share implementations in the Developer Community ## Support Need assistance with timestamp extraction or video synchronization? * **Email**: [support@rhombus.com](mailto:support@rhombus.com) * **Community**: [rhombus.community](https://rhombus.community) * **Documentation**: Browse our complete API reference This advanced implementation guide is regularly updated to reflect the latest best practices for working with Rhombus video segments. # Alarm Monitoring Source: https://api-docs.rhombus.community/implementations/alarm-monitoring Build alarm monitoring integrations with the Rhombus API — track locations, dispatch responders, and manage threat cases and security alerts in real time. ## Overview Rhombus alarm monitoring provides centralized security management for your locations. Through the API, you can enable or disable monitoring per location, respond to threat cases in real time, manage keypad PINs, and retrieve policy alerts. This is especially useful for building custom security dashboards, automating alarm response workflows, or integrating Rhombus alarms into a third-party monitoring platform. When monitoring is enabled for a location, Rhombus cameras and sensors detect security events and generate threat cases. Your application can then retrieve, escalate, dismiss, or cancel those threat cases programmatically. ## Prerequisites Before you begin, make sure you have: * A **Rhombus API key** with alarm monitoring permissions (generated in the Rhombus Console under Settings > API) * An **alarm monitoring license** active on your organization * At least one **configured location** with cameras or sensors enrolled in alarm monitoring ## Check Monitoring Status Use the org-wide status endpoint to see which locations have monitoring enabled, or query a specific location directly. ### Organization-Wide Status ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/orgStatus", headers=headers, json={} ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/orgStatus', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/orgStatus \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` ### Location-Specific Status ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/locationStatus", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/locationStatus', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/locationStatus \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"locationUuid": "YOUR_LOCATION_UUID"}' ``` You can also retrieve the full alarm monitoring settings for a location, including armed state and configured schedules: ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/getAlertMonitoringSettingsForLocation", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/getAlertMonitoringSettingsForLocation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/getAlertMonitoringSettingsForLocation \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"locationUuid": "YOUR_LOCATION_UUID"}' ``` ## Enable and Disable Monitoring Toggle alarm monitoring on or off for a specific location. When you enable monitoring, the system begins watching for security events at that location. When you disable it, threat case generation pauses. ### Enable Monitoring ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/enableMonitoringForLocation", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/enableMonitoringForLocation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/enableMonitoringForLocation \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"locationUuid": "YOUR_LOCATION_UUID"}' ``` ### Disable Monitoring ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/disableMonitoringForLocation", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/disableMonitoringForLocation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/disableMonitoringForLocation \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"locationUuid": "YOUR_LOCATION_UUID"}' ``` You can automate monitoring schedules by enabling and disabling monitoring at specific times using a cron job or scheduler in your application. ## Manage Threat Cases Threat cases represent detected security events that require a response. When a camera or sensor detects suspicious activity at a monitored location, the system creates a threat case. You can then retrieve, escalate, dismiss, or cancel these cases through the API. ### Get Threat Cases Retrieve threat cases within a time range. Use pagination parameters to handle large result sets. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } # Get threat cases from the last 24 hours now_ms = int(time.time() * 1000) one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/event/getAlertMonitoringThreatCases", headers=headers, json={ "afterTimestampMs": one_day_ago_ms, "beforeTimestampMs": now_ms } ) data = response.json() for case in data.get("threatCases", []): print(f"Threat Case: {case['uuid']} - Status: {case['status']}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneDayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/event/getAlertMonitoringThreatCases', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ afterTimestampMs: oneDayAgo, beforeTimestampMs: now }) }); const data = await response.json(); for (const threatCase of data.threatCases || []) { console.log(`Threat Case: ${threatCase.uuid} - Status: ${threatCase.status}`); } ``` ```bash cURL theme={null} # Replace timestamps with current epoch millisecond values curl -X POST https://api2.rhombussystems.com/api/event/getAlertMonitoringThreatCases \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "afterTimestampMs": 1712620800000, "beforeTimestampMs": 1712707200000 }' ``` ### Respond to Threat Cases When a threat case is active, you have three response options depending on the situation. Escalate a threat case to a full alarm when the threat is confirmed and requires immediate response from security or law enforcement. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/escalateThreatCaseToAlarm", headers=headers, json={ "uuid": "YOUR_THREAT_CASE_ID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/escalateThreatCaseToAlarm', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ uuid: 'YOUR_THREAT_CASE_ID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/escalateThreatCaseToAlarm \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"uuid": "YOUR_THREAT_CASE_ID"}' ``` Dismiss a threat case when you have reviewed it and determined it does not require action (for example, a false positive). ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/dismissThreatCase", headers=headers, json={ "uuid": "YOUR_THREAT_CASE_ID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/dismissThreatCase', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ uuid: 'YOUR_THREAT_CASE_ID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/dismissThreatCase \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"uuid": "YOUR_THREAT_CASE_ID"}' ``` Cancel a threat case to stop it from progressing further. Use this when the situation has been resolved before escalation is needed. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/cancelThreatCase", headers=headers, json={ "uuid": "YOUR_THREAT_CASE_ID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/cancelThreatCase', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ uuid: 'YOUR_THREAT_CASE_ID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/cancelThreatCase \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"uuid": "YOUR_THREAT_CASE_ID"}' ``` ## Manage Location PINs PINs allow users to arm and disarm alarm monitoring at a location using a Rhombus keypad. You can create and delete PINs for each location through the API. ### Create a PIN ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/createCustomPinForLocation", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID", "pin": "4829", "name": "Front Desk Staff" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/createCustomPinForLocation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID', pin: '4829', name: 'Front Desk Staff' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/createCustomPinForLocation \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "locationUuid": "YOUR_LOCATION_UUID", "pin": "4829", "name": "Front Desk Staff" }' ``` ### Delete a PIN ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/alertmonitoring/deletePinForLocation", headers=headers, json={ "locationUuid": "YOUR_LOCATION_UUID", "pin": "4829" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/alertmonitoring/deletePinForLocation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ locationUuid: 'YOUR_LOCATION_UUID', pin: '4829' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/alertmonitoring/deletePinForLocation \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "locationUuid": "YOUR_LOCATION_UUID", "pin": "4829" }' ``` Deleting a PIN takes effect immediately. Make sure the PIN is no longer needed before removing it, as users relying on that PIN will lose the ability to arm or disarm the system at the keypad. ## Work with Policy Alerts Policy alerts are triggered when configured detection policies (such as motion detection or person detection) fire at monitored locations. Use these endpoints to retrieve recent alerts and dismiss them after review. ### Get Policy Alerts ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/event/getPolicyAlerts", headers=headers, json={} ) data = response.json() for alert in data.get("policyAlerts", []): print(f"Alert: {alert['uuid']} - Type: {alert.get('type', 'N/A')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/event/getPolicyAlerts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); for (const alert of data.policyAlerts || []) { console.log(`Alert: ${alert.uuid} - Type: ${alert.type || 'N/A'}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/event/getPolicyAlerts \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` ### Dismiss a Policy Alert ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/event/dismissPolicyAlertV2", headers=headers, json={ "alertUuid": "YOUR_POLICY_ALERT_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/event/dismissPolicyAlertV2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ alertUuid: 'YOUR_POLICY_ALERT_UUID' }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/event/dismissPolicyAlertV2 \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"alertUuid": "YOUR_POLICY_ALERT_UUID"}' ``` ## Next Steps Receive real-time notifications when alarm events occur so your application can respond immediately Explore the full API reference for complete request and response schemas for all alarm monitoring endpoints # Backup Footage to Local Storage Source: https://api-docs.rhombus.community/implementations/backup-footage-local-storage Download and archive Rhombus camera footage to local storage like NAS or external drives using the API for compliance, retention, and offline access. **Community Example - Not Officially Supported** This guide is provided as a community example and is **not officially supported** by Rhombus Systems. While Rhombus does not officially support on-premise backup, this guide demonstrates how to implement a local backup solution using the Rhombus API. Use this implementation at your own discretion and ensure it meets your organization's backup and compliance requirements. ## Overview This guide demonstrates how to backup video and audio footage from Rhombus cameras to local storage devices such as Network Attached Storage (NAS), external hard drives, or local servers. The solution uses a Python script that leverages the Rhombus API to download footage in parallel across multiple cameras. The implementation supports: * **Multi-camera downloads** with threading for improved performance * **Video and audio synchronization** with automatic merging * **Flexible scheduling** using cron jobs or task schedulers * **Customizable time ranges** for historical footage backup * **Location-based filtering** to backup specific sites ## How It Works The script queries the Rhombus API to enumerate all cameras in your organization, filtering by connection status, location, or specific camera UUIDs. A federated session token is generated for each camera, providing temporary (1-hour) credentials for media access without exposing your API key in download URLs. For each camera, the script: 1. Requests MPD (MPEG-DASH) playlist URIs for video and audio streams 2. Downloads the initialization segment (`seg_init.mp4`) 3. Downloads sequential 2-second media segments 4. Writes segments to local storage If audio is available, the script uses FFmpeg to merge video and audio streams into a single file, then cleans up temporary files. ## Prerequisites Before implementing local backup, ensure you have: * **Python 3.7 or higher** installed on your backup system * **Rhombus API key** from the [Rhombus Console](https://console.rhombussystems.com) * **Network connectivity** to Rhombus cameras (LAN or WAN) * **Sufficient storage space** on your backup device (estimate 1-2 GB per camera per day) * **FFmpeg** installed for audio-video merging Calculate your storage requirements based on camera count, retention period, and recording quality. A typical Rhombus camera generates approximately 40-60 GB per month of footage. ## Installation ### Install Required Software ```bash theme={null} # Update package list sudo apt update # Install Python 3 and pip sudo apt install python3 python3-pip # Install FFmpeg sudo apt install ffmpeg # Install Git (to clone the repository) sudo apt install git ``` ```bash theme={null} # Install Homebrew if not already installed /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Install Python 3 brew install python # Install FFmpeg brew install ffmpeg # Install Git brew install git ``` ```powershell theme={null} # Install Python from python.org # Download from: https://www.python.org/downloads/ # Install FFmpeg # Download from: https://ffmpeg.org/download.html#build-windows # Add FFmpeg to your PATH environment variable # Install Git # Download from: https://git-scm.com/download/win ``` ```bash theme={null} # Enable SSH in Control Panel > Terminal & SNMP # Install Python via Package Center # Search for and install "Python 3" # Install ffmpeg via SynoCommunity # Add SynoCommunity package source: https://synocommunity.com/ # Install ffmpeg from Community packages # Install Git via Package Center ``` ### Download the Backup Script Clone the Rhombus API examples repository: ```bash theme={null} # Clone the repository git clone https://github.com/RhombusSystems/api-examples-python.git # Navigate to the NAS backup directory cd api-examples-python/NAS-Backup-v2 # Install Python dependencies pip3 install -r requirements.txt ``` ### Verify Installation Test that all components are installed correctly: ```bash theme={null} # Check Python version python3 --version # Check FFmpeg installation ffmpeg -version # Check pip packages pip3 list | grep -E "(requests|ffmpeg|urllib3|xmltodict)" ``` ## Configuration ### Command-Line Parameters The backup script supports the following parameters: Your Rhombus API key for authentication. Required unless using certificate-based authentication. **Example**: `--api_key YOUR_API_KEY_HERE` Unix epoch timestamp for backup start time. Defaults to 1 hour ago if not specified. **Example**: `--start_time 1693526400` (August 31, 2023 at 16:00:00 UTC) Duration in seconds to backup from start time. Defaults to 3600 seconds (1 hour). **Example**: `--duration 7200` (2 hours) Filter backup to cameras at a specific location. Useful for multi-site deployments. **Example**: `--location_uuid location-uuid-here` Backup footage from a specific camera only. **Example**: `--camera_uuid camera-uuid-here` Use WAN addresses instead of LAN. Enable when backup system is outside your local network. **Example**: `--usewan` Enable debug logging for troubleshooting. **Example**: `--debug` Path to client certificate for mTLS authentication (advanced use case). **Example**: `--cert /path/to/cert.pem` Path to private key for mTLS authentication (advanced use case). **Example**: `--private_key /path/to/key.pem` ## Usage Examples ### Basic Backup (Last Hour) Backup the last hour of footage from all cameras: ```bash theme={null} python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY ``` ### Specific Time Range Backup footage from a specific 2-hour window: ```bash theme={null} python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY \ --start_time 1693526400 \ --duration 7200 ``` ### Single Camera Backup Backup footage from one specific camera: ```bash theme={null} python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY \ --camera_uuid camera-uuid-here \ --duration 3600 ``` ### Location-Based Backup Backup all cameras at a specific location: ```bash theme={null} python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY \ --location_uuid location-uuid-here \ --duration 3600 ``` ### WAN Access (Remote Backup) Backup from outside your local network: ```bash theme={null} python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY \ --usewan \ --duration 3600 ``` ### Debug Mode Enable detailed logging for troubleshooting: ```bash theme={null} python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY \ --debug \ --duration 3600 ``` ## Output Files Downloaded footage files follow this naming convention: ```text theme={null} {CameraName}_{CameraUUID}_{Timestamp}_{Type}.{Extension} ``` **Examples:** ```text theme={null} FrontDoor_abc123def456_1693526400_video.mp4 Lobby_def789ghi012_1693526400_videoWithAudio.mp4 Warehouse_ghi345jkl678_1693526400_video.mp4 ``` **File Types:** * **Video-only files**: `.mp4` format (when no audio is available) * **Merged files**: `.mp4` format (video + audio combined via FFmpeg) * **Temporary files**: `.webm` format (automatically deleted after merging) The script automatically cleans up temporary files after successful merging. Only final `.mp4` files remain in your backup directory. ## Scheduling Automated Backups ### Using Cron (Linux/macOS/NAS) Create automated backups using cron jobs: ### Edit Crontab Open your crontab configuration: ```bash theme={null} crontab -e ``` ### Add Backup Schedule Add one of the following examples based on your needs: ```bash Hourly Backup theme={null} # Backup every hour at minute 0 0 * * * * cd /path/to/api-examples-python/NAS-Backup-v2 && /usr/bin/python3 copy_footage_script_threading.py --api_key YOUR_API_KEY --duration 3600 >> /var/log/rhombus_backup.log 2>&1 ``` ```bash Daily Backup (Midnight) theme={null} # Backup last 24 hours at midnight 0 0 * * * cd /path/to/api-examples-python/NAS-Backup-v2 && /usr/bin/python3 copy_footage_script_threading.py --api_key YOUR_API_KEY --duration 86400 >> /var/log/rhombus_backup.log 2>&1 ``` ```bash Every 4 Hours theme={null} # Backup every 4 hours 0 */4 * * * cd /path/to/api-examples-python/NAS-Backup-v2 && /usr/bin/python3 copy_footage_script_threading.py --api_key YOUR_API_KEY --duration 14400 >> /var/log/rhombus_backup.log 2>&1 ``` ```bash Business Hours Only theme={null} # Backup every 2 hours during business hours (8 AM - 6 PM, Mon-Fri) 0 8-18/2 * * 1-5 cd /path/to/api-examples-python/NAS-Backup-v2 && /usr/bin/python3 copy_footage_script_threading.py --api_key YOUR_API_KEY --duration 7200 >> /var/log/rhombus_backup.log 2>&1 ``` ### Save and Verify Save the crontab and verify it's scheduled: ```bash theme={null} # List current cron jobs crontab -l # Check cron service status sudo systemctl status cron ``` **Cron Schedule Reference:** * `0 * * * *` - Every hour at minute 0 * `*/30 * * * *` - Every 30 minutes * `0 */4 * * *` - Every 4 hours * `0 0 * * *` - Daily at midnight * `0 2 * * 0` - Weekly on Sunday at 2 AM ### Using Task Scheduler (Windows) ### Open Task Scheduler Press `Win + R`, type `taskschd.msc`, and press Enter. ### Create New Task 1. Click **"Create Task"** in the right panel 2. Name it **"Rhombus Footage Backup"** 3. Select **"Run whether user is logged on or not"** ### Set Trigger 1. Go to the **Triggers** tab 2. Click **New** 3. Choose frequency (Daily, Weekly, etc.) 4. Set start time and recurrence ### Configure Action 1. Go to the **Actions** tab 2. Click **New** 3. **Action**: Start a program 4. **Program**: `C:\Python39\python.exe` (adjust path) 5. **Arguments**: `copy_footage_script_threading.py --api_key YOUR_API_KEY --duration 3600` 6. **Start in**: `C:\path\to\NAS-Backup-v2` ### Save and Test 1. Click **OK** to save 2. Right-click the task and select **"Run"** to test ## API Endpoints Used The backup script interacts with the following Rhombus API endpoints: ### Camera Enumeration ```text theme={null} POST https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList ``` Retrieves list of cameras with connection status and location information. ### Audio Gateway List ```text theme={null} POST https://api2.rhombussystems.com/api/audiogateway/getMinimalAudioGatewayStateList ``` Fetches audio devices associated with cameras. ### Session Token Generation ```text theme={null} POST https://api2.rhombussystems.com/api/org/generateFederatedSessionToken ``` Creates temporary session credentials (1-hour validity) for secure media access. ### Video Media URIs ```text theme={null} POST https://api2.rhombussystems.com/api/camera/getMediaUris ``` Obtains MPD (MPEG-DASH) playlist templates for video streams. ### Audio Media URIs ```text theme={null} POST https://api2.rhombussystems.com/api/audiogateway/getMediaUris ``` Obtains MPD templates for audio streams. All API calls require authentication using the `x-auth-scheme: api-token` header with your API key, or certificate-based mTLS authentication. ## Performance Optimization ### Threading Configuration The script uses Python's `ThreadPoolExecutor` with a maximum of 4 concurrent workers. This balances download speed with API rate limits and system resources. To adjust thread count, modify the script: ```python theme={null} # In copy_footage_script_threading.py with ThreadPoolExecutor(max_workers=4) as executor: # Change 4 to desired value ``` **Recommendations:** * **2-4 workers**: Standard NAS or low-end systems * **4-8 workers**: High-performance NAS or servers * **8-16 workers**: Enterprise servers with high bandwidth Increasing thread count beyond recommended values may trigger rate limiting or overload your network/storage. ### Storage Considerations **Calculate Required Space:** ```text theme={null} Storage (GB) = Cameras × Days × 1.5 GB/day ``` **Example:** * 10 cameras × 30 days × 1.5 GB = 450 GB required **Best Practices:** * Maintain at least 20% free space on backup device * Implement retention policies to delete old footage * Monitor disk usage regularly * Use compression if long-term archival is needed ### Network Optimization **LAN vs WAN:** * **LAN Mode** (default): Faster downloads, uses local network addresses * **WAN Mode** (`--usewan`): Required for remote backup, slower but accessible from anywhere **Bandwidth Requirements:** * Approximately 2-4 Mbps per concurrent camera download * 4 workers = 8-16 Mbps recommended bandwidth ## Retention and Cleanup Implement a retention policy to manage storage usage: ```bash Delete Files Older Than 30 Days theme={null} # Create cleanup script cat > cleanup_old_footage.sh << 'EOF' #!/bin/bash BACKUP_DIR="/path/to/backup/directory" RETENTION_DAYS=30 find "$BACKUP_DIR" -name "*.mp4" -type f -mtime +$RETENTION_DAYS -delete find "$BACKUP_DIR" -name "*.webm" -type f -mtime +$RETENTION_DAYS -delete echo "Deleted footage older than $RETENTION_DAYS days" EOF chmod +x cleanup_old_footage.sh ``` ```bash Automated Weekly Cleanup (Cron) theme={null} # Add to crontab (runs every Sunday at 3 AM) 0 3 * * 0 /path/to/cleanup_old_footage.sh >> /var/log/rhombus_cleanup.log 2>&1 ``` ```python Python Cleanup Script theme={null} import os import time from pathlib import Path def cleanup_old_files(backup_dir, retention_days): """Remove footage files older than retention period""" cutoff_time = time.time() - (retention_days * 86400) removed_count = 0 freed_space = 0 for file in Path(backup_dir).glob("**/*.mp4"): if file.stat().st_mtime < cutoff_time: size = file.stat().st_size file.unlink() removed_count += 1 freed_space += size print(f"Removed {removed_count} files") print(f"Freed {freed_space / (1024**3):.2f} GB") # Usage cleanup_old_files("/path/to/backup", retention_days=30) ``` ## Troubleshooting **Symptoms:** * `401 Unauthorized` errors * `Invalid API key` messages **Solutions:** 1. Verify API key is correct in Rhombus Console 2. Ensure API key has camera access permissions 3. Check that `x-auth-scheme` header is set correctly 4. Regenerate API key if compromised ```bash theme={null} # Test API authentication curl -X POST https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" ``` **Symptoms:** * Downloads take longer than expected * Network timeout errors **Solutions:** 1. Use LAN mode instead of WAN if backing up locally 2. Reduce thread count in `ThreadPoolExecutor` 3. Check network bandwidth and camera connectivity 4. Verify storage device write speed 5. Consider scheduling backups during off-peak hours ```bash theme={null} # Test with fewer threads and debug mode python3 copy_footage_script_threading.py \ --api_key YOUR_API_KEY \ --debug \ --duration 600 # Start with 10 minutes ``` **Symptoms:** * Temporary `.webm` files remain * No merged `.mp4` output * FFmpeg error messages **Solutions:** 1. Verify FFmpeg is installed and in PATH 2. Check that both video and audio files were downloaded 3. Ensure sufficient disk space for temporary files 4. Update FFmpeg to latest version ```bash theme={null} # Check FFmpeg installation which ffmpeg ffmpeg -version # Manual merge test ffmpeg -i video.webm -i audio.webm -c copy output.mp4 ``` **Symptoms:** * Script reports 0 cameras to backup * Empty camera list **Solutions:** 1. Verify cameras are online in Rhombus Console 2. Check location UUID filter if specified 3. Ensure API key has access to cameras 4. Review camera connection status filters in script ```bash theme={null} # List all cameras via API curl -X POST https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ | python3 -m json.tool ``` **Symptoms:** * Script fails with write errors * `No space left on device` messages **Solutions:** 1. Check available disk space: `df -h` 2. Implement retention policy to delete old footage 3. Reduce backup duration or frequency 4. Add additional storage capacity 5. Use compression for archived footage ```bash theme={null} # Check disk usage df -h /path/to/backup # Find largest files du -sh /path/to/backup/* | sort -hr | head -10 ``` **Symptoms:** * Scheduled backups don't execute * No new footage files created **Solutions:** 1. Check cron service status: `systemctl status cron` 2. Verify crontab syntax: `crontab -l` 3. Check cron logs: `grep CRON /var/log/syslog` 4. Ensure script paths are absolute 5. Verify user permissions ```bash theme={null} # Test cron job manually cd /path/to/NAS-Backup-v2 /usr/bin/python3 copy_footage_script_threading.py --api_key YOUR_API_KEY # Check cron logs tail -f /var/log/syslog | grep CRON ``` ## Best Practices Never hardcode API keys in scripts. Use environment variables or secure key management systems. Rotate API keys regularly. Set up monitoring and alerting for backup failures. Review logs regularly to ensure backups complete successfully. Regularly test restoring footage from backups to verify integrity and confirm your recovery process works. Define and enforce retention policies to manage storage costs and comply with data protection regulations. Consider multiple backup locations (onsite + offsite) for critical footage. Implement the 3-2-1 backup rule. Maintain documentation of your backup configuration, schedules, and recovery procedures for your team. ## Security Considerations Follow these security best practices when implementing local backup: ### API Key Protection 1. **Store securely**: Use environment variables or credential managers 2. **Restrict access**: Limit file permissions on scripts containing keys 3. **Rotate regularly**: Change API keys periodically 4. **Audit usage**: Monitor API key activity in Rhombus Console ```bash theme={null} # Store API key in environment variable export RHOMBUS_API_KEY="your-api-key-here" # Use in script python3 copy_footage_script_threading.py --api_key "$RHOMBUS_API_KEY" # Restrict script permissions chmod 600 backup_script.sh ``` ### Network Security * Use LAN mode when possible to avoid WAN exposure * Implement firewall rules to restrict access * Consider VPN for remote backup scenarios * Enable mTLS authentication for enhanced security ### Storage Security * Encrypt backup storage devices * Restrict file system permissions * Implement access controls on NAS/server * Regularly audit who has access to backup files ## Compliance Considerations When implementing local backup, consider: **Data Retention:** * Follow your organization's data retention policies * Comply with industry regulations (HIPAA, GDPR, etc.) * Document retention periods and deletion procedures **Access Control:** * Maintain audit logs of who accesses backup footage * Implement role-based access controls * Document authorized personnel **Data Protection:** * Encrypt data at rest and in transit * Implement secure deletion procedures * Regular security assessments ## Advanced Configuration ### Custom Output Directory Modify the script to save files to a specific directory: ```python theme={null} # In copy_footage_script_threading.py OUTPUT_DIR = "/mnt/nas/rhombus_backups" # Create directory structure by date import datetime date_dir = datetime.datetime.now().strftime("%Y-%m-%d") output_path = os.path.join(OUTPUT_DIR, date_dir) os.makedirs(output_path, exist_ok=True) ``` ### Email Notifications Add email alerts for backup completion or failures: ```python theme={null} import os import smtplib from email.mime.text import MIMEText def send_notification(status, message): msg = MIMEText(message) msg['Subject'] = f'Rhombus Backup {status}' msg['From'] = 'backup@yourcompany.com' msg['To'] = 'admin@yourcompany.com' with smtplib.SMTP('smtp.gmail.com', 587) as server: server.starttls() server.login(os.environ['SMTP_EMAIL'], os.environ['SMTP_PASSWORD']) server.send_message(msg) # Use after backup completes try: # ... backup code ... send_notification("Success", "All cameras backed up successfully") except Exception as e: send_notification("Failed", f"Backup failed: {str(e)}") ``` ### Webhook Integration Trigger webhooks after backup completion: ```python theme={null} import datetime import requests def trigger_webhook(status, cameras_backed_up): webhook_url = "https://your-webhook-endpoint.com/backup" payload = { "status": status, "timestamp": datetime.datetime.now().isoformat(), "cameras": cameras_backed_up } requests.post(webhook_url, json=payload) ``` ## Next Steps Learn how to stream live footage from Rhombus cameras Set up webhooks to receive real-time events Access the complete backup script source code Ask questions and share your implementation ## Additional Resources * **Rhombus API Documentation**: Complete API reference for all endpoints * **Python Requests Documentation**: [docs.python-requests.org](https://docs.python-requests.org/) * **FFmpeg Documentation**: [ffmpeg.org/documentation](https://ffmpeg.org/documentation.html) * **Cron Documentation**: [man7.org/linux/man-pages/man5/crontab.5.html](https://man7.org/linux/man-pages/man5/crontab.5.html) For questions, issues, or to share your backup implementation, visit the [Rhombus Developer Community](https://rhombus.community) and post in the Guides & Resources section. # EdgeCaster RTSP Gateway Source: https://api-docs.rhombus.community/implementations/edgecaster-rtsp Run EdgeCaster on a Raspberry Pi 5 or any Ubuntu/Debian machine to re-broadcast Rhombus camera streams as sub-second-latency RTSP for legacy VMS, NVR, and third-party video systems. **EdgeCaster** is a local edge gateway that pulls Rhombus Secure Raw Streams (H.264 over HTTPS) and re-broadcasts them as standard RTSP on your local network using bundled [MediaMTX](https://github.com/bluenviron/mediamtx). It runs on a Raspberry Pi 5, a mini-PC, or any Ubuntu/Debian machine, so legacy VMS, NVR, and third-party AI/video systems that require RTSP can consume video from Rhombus cameras with sub-second added latency. You manage it from a simple web dashboard; it runs 24/7 and heals itself when a stream drops. Rhombus does not support RTSP natively because it is not a secure protocol. EdgeCaster bridges this gap for environments that require RTSP compatibility by converting Rhombus's encrypted raw streams locally on your network. Deploy EdgeCaster only on trusted networks. ## How It Works ```text theme={null} Rhombus Camera │ │ Secure Raw Stream (HTTPS H.264) ▼ EdgeCaster (Raspberry Pi 5 / mini-PC / Ubuntu-Debian machine) │ FFmpeg stream copy (no transcoding) ▼ MediaMTX (RTSP server, port 8554) │ ▼ External Systems (VMS / NVR / AI) ``` EdgeCaster uses the Rhombus API to discover cameras, creates secure raw streams via `createRawHttpStream`, and pipes them through FFmpeg (stream copy — no transcoding) to MediaMTX, which serves them as standard RTSP. ### Sub-Second Latency, Always the Latest Frame Because streams are copied rather than transcoded (`-c copy`), there is no decode/encode delay. EdgeCaster minimizes latency by stripping buffering at every hop instead: * **FFmpeg** runs with no input buffering, low-delay flags, reduced probe/analyze time for fast startup, and zeroed mux delay and preload, so packets are forwarded the instant they arrive. The tunables (probe size, analyze duration, stall threshold) are exposed in `/etc/edgecaster/config.yaml` if you need to adjust them on-device. * **MediaMTX** is tuned for low latency and fast failure detection: its control API is enabled on localhost, and a short read timeout drops a stalled publisher quickly. Each RTSP reader gets its own queue, so a slow consumer cannot add latency to the publisher or to other readers. The result: EdgeCaster always pushes the newest frame with sub-second added latency inside the device. End-to-end ("glass-to-glass") latency also depends on the camera's GOP/keyframe interval and your consuming system's own buffer, which EdgeCaster doesn't control. ### Unlimited Concurrent Streams There is no fixed stream limit — the device runs as many streams as its network and CPU allow. Because stream copy is very light, real-world testing on a Raspberry Pi 5 showed a handful of 1080p streams plus multiple RTSP readers at roughly 3% CPU, leaving large headroom. `max_streams: 0` (the default) means unlimited; set a positive value in `config.yaml` if you want an optional hard safety ceiling. ### 24/7 Self-Healing EdgeCaster monitors each stream at the **frame level** using FFmpeg's progress output — not just "is the process alive" — so a frozen-but-alive feed (no new frames) is detected within seconds by a watchdog. All failure triggers (process exit, frozen feed, dead process) funnel through one recovery path that guarantees exactly one relaunch, with no double-restart races. Recovery re-fetches a fresh Secure Raw Stream URL from Rhombus (stream tokens expire) and resumes, typically within seconds. If a stream keeps failing, EdgeCaster retries fast (every 5 seconds, up to 10 times), then backs off to every 5 minutes — and keeps trying forever. It never permanently gives up on a 24/7 device. Frozen-feed recovery uses a lighter, faster backoff. ## Features * Sub-second added latency, always-latest-frame restreaming (stream copy, no transcoding) * Unlimited concurrent streams — as many as your device's network and CPU allow * 24/7 self-healing with frame-level stall detection and automatic stream URL refresh * Live health dashboard with real-time metrics over Server-Sent Events * Webhook alerts for stream drops and device strain (Slack, Make.com, or any HTTP listener) * Live logs viewer in the dashboard — no SSH needed * Optional one-click secure public access via a Cloudflare quick tunnel * Automatic camera discovery via the Rhombus API * Persistent stream state across reboots; streams auto-restore on startup * Nightly auto-updates, a one-line installer, and a ready-to-flash Raspberry Pi image ## Requirements | Component | Requirement | | ---------------- | -------------------------------------------------------------------------------- | | **Device** | Raspberry Pi 5 (8 GB RAM recommended) or any Ubuntu/Debian machine (mini-PC, VM) | | **Architecture** | arm64, amd64, or armv7 | | **Network** | Wired Gigabit Ethernet | | **OS** | Ubuntu or Debian (the Pi image ships Ubuntu Server 24.04) | | **Rhombus** | Org API Key | ## Install On any machine running **Ubuntu or Debian** (Raspberry Pi, mini-PC, or virtual machine), run: ```bash theme={null} curl -fsSL https://raw.githubusercontent.com/RhombusSystems/edgecaster-stream-converter/main/scripts/bootstrap.sh | sudo bash ``` The script downloads and installs everything — FFmpeg, MediaMTX, systemd services — then prints the web address to open. Flash a ready-to-use image — no typing required: Ask Rhombus for the EdgeCaster SD-card image, or build one yourself (see the next tab). Write the `.img.xz` file to an SD card with the free [Raspberry Pi Imager](https://www.raspberrypi.com/software/). Put the card in a **Raspberry Pi 5**, connect it to your network with an Ethernet cable, and power it on. Wait a few minutes, then open `http://edgecaster.local` in a web browser. On first boot the device sets its hostname to `edgecaster`, creates the `edgecaster` user (default password: `edgecaster`), and starts all services. Change the default password immediately after first login: `passwd edgecaster` Install from a git checkout: ```bash theme={null} git clone https://github.com/RhombusSystems/edgecaster-stream-converter.git cd edgecaster-stream-converter sudo bash scripts/install.sh ``` The installer keeps a git checkout on the device, so manual installs receive nightly auto-updates just like the Pi image. To build a flashable SD-card image instead: ```bash theme={null} # Cloud-init mode (smaller image, needs internet on first boot) sudo bash image/build-image.sh # Pre-baked mode (larger image, no internet needed on first boot) sudo bash image/build-image.sh --prebaked ``` Output is a flashable `.img.xz`. ## Set Up (2 Minutes) Open EdgeCaster in a web browser: `http://edgecaster.local` or `http://`. Your cameras appear automatically. Flip the switch next to any camera to start its RTSP stream. Copy the camera's RTSP link into your VMS, NVR, or AI system: ```text theme={null} rtsp://:8554/front_door rtsp://:8554/warehouse rtsp://:8554/parking_lot ``` Camera names are normalized to URL-safe slugs and persisted, so RTSP paths survive reboots. Test with VLC: `vlc rtsp://:8554/` ## Web Dashboard The dashboard is a clean, responsive operator console that works on any device or browser — on mobile, the sidebar becomes a drawer. A persistent status pulse (live stream count and a health dot) is always visible in the top bar. ### Live Health Metrics The dashboard streams metrics live (about once per second) over Server-Sent Events — no manual refresh: active streams, cameras found, CPU %, memory %, temperature, power/throttle status (under-voltage and throttling), 1-minute load average, uptime, and per-stream throughput and reader counts. **Raspberry Pi temperature and power/throttle metrics require the Raspberry Pi kernel** (`linux-raspi`). On the Ubuntu `-generic`/`virtual` kernel these read "N/A" because the OS exposes no thermal sensors. Fix: `sudo apt-get install -y linux-raspi && sudo reboot`. Everything else works regardless. ### Webhook Alerts EdgeCaster can send an alert to a generic webhook — a Slack incoming webhook, Make.com, or any HTTP listener — when: * A stream drops or can't recover * The Pi reports **under-voltage** (power constrained) * The Pi reports **thermal throttling** * CPU or load average stays high Thresholds are configurable, each alert has a cooldown, and alerts clear on recovery so the webhook isn't spammed. Set it up under **Settings → Alerts**, which includes a **Send test alert** button. Active alerts also show on the dashboard. ### Live Logs A **Logs** tab (under **Cameras**) shows the device's logs live in a terminal-style console — useful for diagnosing a camera or connection issue without SSH: * Sources: **Application**, **Streams**, and **Rhombus API** * New lines stream in live, with color-coded log levels * Pause and clear controls ## Public Access (Optional) To reach the dashboard from outside your network, go to **Settings → Public Access**, set a **username and password**, and click **Turn on public access**. EdgeCaster creates a secure public link via a **Cloudflare quick tunnel** and shows it. No Cloudflare account, token, or configuration is required. The access model: on your **local network you never log in**; the **public link always requires** the username and password. Only traffic arriving over the public tunnel is challenged (HTTP Basic auth); the password is stored hashed, never in plaintext. * The public link is **ephemeral** — it changes each time public access restarts. That's the trade-off for needing zero Cloudflare credentials. * The link exposes camera controls: use a strong password and turn public access off when you don't need it. * Only the web dashboard is exposed publicly — **RTSP is not tunneled** and stays on the LAN. Public access requires the `cloudflared` helper, which the installer includes. ## Network Ports | Port | Purpose | | -------- | ----------------------------------------------- | | **80** | Web UI | | **8554** | RTSP streams (MediaMTX) | | 8000 | Backend API (internal, localhost only) | | 9997 | MediaMTX control API (internal, localhost only) | ## Rhombus API Endpoints Used EdgeCaster interacts with 5 Rhombus API endpoints automatically: | Endpoint | Purpose | | -------------------------------------------- | --------------------------------------- | | `POST /api/camera/getMinimalCameraStateList` | Discover cameras in your organization | | `POST /api/camera/createRawHttpStream` | Create a secure raw stream for a camera | | `POST /api/camera/deleteRawHttpStream` | Clean up stream when disabled | | `POST /api/camera/getRawHttpStreams` | List existing raw streams | | `POST /api/location/getLocationLabelsForOrg` | Resolve location names for the UI | ## Managing Services EdgeCaster runs as systemd services (`edgecaster` and `mediamtx`) with watchdog integration. Enabled cameras and their RTSP slugs persist across reboots, and streams auto-restore on startup. ```bash theme={null} # Check status sudo systemctl status edgecaster mediamtx # View logs journalctl -u edgecaster -f # Restart sudo systemctl restart edgecaster # Update now (also runs nightly on its own) sudo bash /opt/edgecaster/scripts/edgecaster-update.sh # Uninstall sudo bash /opt/edgecaster/scripts/uninstall.sh ``` ## Auto-Updates EdgeCaster checks for updates hourly and applies them (git fast-forward) during a configurable window (default: 2:00–5:00 AM). All install methods auto-update, including manual git-checkout installs. Configure via the web UI Settings page or directly: ```yaml theme={null} # /etc/edgecaster/config.yaml auto_update_enabled: true update_hour_start: 2 update_hour_end: 5 ``` ## Configuration Configuration lives in `/etc/edgecaster/config.yaml`; runtime state (enabled cameras and RTSP slugs) lives in `/var/lib/edgecaster/state.json`; logs live in `/var/log/edgecaster/`. Notable keys: ```yaml theme={null} # /etc/edgecaster/config.yaml max_streams: 0 # 0 = unlimited (default); positive value = hard ceiling stall_threshold_seconds: 6 # frozen-feed detection threshold ffmpeg_probesize: 500000 # lower = faster startup ffmpeg_analyzeduration: 1000000 alerts_enabled: false alert_webhook_url: "" cpu_alert_threshold: 85 # percent, sustained temp_alert_threshold_c: 80 load_alert_threshold: 0 # 1-min load average; 0 = disabled ``` ## Local Development ```bash Backend theme={null} python3 -m venv venv source venv/bin/activate pip install -r requirements.txt cd backend uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` ```bash Frontend theme={null} cd frontend npm install npm run dev # Dev server on http://localhost:5173, proxies /api to backend ``` ```bash Tests theme={null} pip install pytest pytest-asyncio python -m pytest backend/tests/ -v ``` ## EdgeCaster API The EdgeCaster backend exposes a local REST API: | Method | Path | Description | | ------ | ----------------------------------------- | --------------------------------------------- | | `GET` | `/api/auth/status` | Check setup state | | `GET` | `/api/settings` | Get app settings | | `POST` | `/api/settings/api-key` | Set Rhombus API key | | `PUT` | `/api/settings/update-schedule` | Configure auto-update window | | `PUT` | `/api/settings/alerts` | Configure webhook alerts | | `POST` | `/api/settings/alerts/test` | Send a test alert | | `GET` | `/api/settings/public-access` | Get public access status | | `POST` | `/api/settings/public-access/credentials` | Set public access username/password | | `POST` | `/api/settings/public-access/enable` | Turn on public access | | `POST` | `/api/settings/public-access/disable` | Turn off public access | | `POST` | `/api/settings/discovery/refresh` | Refresh camera list from Rhombus | | `GET` | `/api/cameras` | List discovered cameras | | `GET` | `/api/streams` | List active RTSP streams | | `POST` | `/api/streams/{uuid}/enable` | Start RTSP stream for a camera | | `POST` | `/api/streams/{uuid}/disable` | Stop RTSP stream | | `GET` | `/api/system/status` | System health snapshot | | `GET` | `/api/system/stream` | Live system status (Server-Sent Events, \~1s) | | `GET` | `/api/logs` | Log snapshot | | `GET` | `/api/logs/stream` | Live log tail (Server-Sent Events) | | `GET` | `/api/logs/sources` | List available log sources | ## Limitations * RTSP authentication is not enabled in v1 — restrict access via network controls (can be added via MediaMTX config) * Consuming systems must be on the local network — RTSP is not tunneled by public access * Secure raw stream tokens expire automatically; EdgeCaster re-fetches them on recovery * No login on the LAN web UI — trusted network deployment only (public access adds a login for the public tunnel only) ## Security Notes * Your API key is stored in `/etc/edgecaster/config.yaml` (owned by the `edgecaster` user, not world-readable) * API keys are never logged or exposed in API responses * The systemd service runs with hardened security: `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp` * RTSP streams are unauthenticated — restrict port 8554 access at the network level * With public access on, only tunnel traffic is challenged with HTTP Basic auth; the password is stored hashed (never plaintext) and LAN traffic is never challenged ## Troubleshooting Verify your Rhombus API key is valid and has access to cameras. Check that the device can reach `api2.rhombussystems.com`. Try refreshing discovery from the Settings page. Check FFmpeg is installed (`ffmpeg -version`), MediaMTX is running (`systemctl status mediamtx`), and the camera is online. Check the **Logs** tab under Cameras, or view logs with `journalctl -u edgecaster -f`. Confirm the stream shows "running" in the web UI. Test with VLC: `vlc rtsp://:8554/`. Check that port 8554 is not blocked by a firewall. On a Raspberry Pi running the Ubuntu `-generic` or `virtual` kernel, the OS exposes no thermal sensors. Install the Pi kernel: `sudo apt-get install -y linux-raspi && sudo reboot`. All other metrics work regardless. The public link is ephemeral — it changes each time public access restarts (for example after a reboot). Open **Settings → Public Access** on the LAN to see the current link. Check the timer: `systemctl status edgecaster-update.timer`. Verify the installation is git-based (`.git` folder exists) and the current time falls within the update window. ## Need Help? Email [support@rhombus.com](mailto:support@rhombus.com) and we'll help you get set up. It helps to include what you were doing and anything shown on the dashboard. ## Resources Source code, image builder, and issue tracker Direct streaming integration without RTSP conversion Embed camera streams in React applications Full documentation for raw stream and camera endpoints # Face Recognition & People Counting Source: https://api-docs.rhombus.community/implementations/faces-people-counting Enroll and manage known people, query face-match events, tune the matching threshold, and pull camera people-counting analytics through the Rhombus API. ## Overview This guide covers two distinct camera analytics capabilities that are easy to confuse: * **Face Recognition (identity)** — enroll known people from face images, then receive **face-match events** when a camera detects a matching face. Each event carries a confidence score and the best-matching person. Use this to recognize *who* appears on camera. * **People counting (headcount)** — aggregate, non-identifying camera analytics that count *how many* people cross a camera's field of view over time. These come from the Report Webservice as count time series and occupancy counts. No identity is involved. Through the API you can: * **Enroll and manage people** by creating person records and uploading face images * **Query face-match events** with rich filters and pagination * **Tune the matching threshold** and correct misidentified events * **Pull people-count analytics** as time series or the most recent counts per camera **Not the same as occupancy sensors.** Rhombus also offers physical occupancy sensors (Bluetooth/motion hardware) that report room presence. Those are a separate product served by a different API — see the [IoT Sensors](/implementations/iot-sensors) guide. This page's "people counting" is *camera* analytics. **Biometric privacy.** Face recognition processes biometric data. Many jurisdictions regulate the collection, storage, and use of biometric identifiers, and some require notice or consent. Review the biometric-privacy laws that apply to your locations before enrolling faces, and confirm your organization's policies. This guide describes only the API surface — it is not legal advice. ## Prerequisites Before you begin, make sure you have: * A **Rhombus API key**. Enrolling or managing faces requires **face-management permissions**; reading people-count reports requires report read access. Generate and scope keys in the Rhombus Console under Settings > API. * At least one **camera** with the relevant analytics enabled (face recognition for identity workflows, people counting for headcount analytics). * **Face Recognition is a licensed, gated feature.** If the face endpoints are unavailable for your organization, contact your Rhombus account team to confirm entitlement. All requests are `POST` to `https://api2.rhombussystems.com` and authenticate with the `x-auth-scheme` and `x-auth-apikey` headers. Timestamps are epoch milliseconds unless noted, and match confidence is a float between `0` and `1`. ## Enroll and manage known people A **person** is a named identity record. A **matchmaker** is an enrolled face template attached to a person; the recognition engine compares detected faces against your matchmakers. ### Create a person Create a named person record to enroll against. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/person/createPerson", headers=headers, json={"name": "Jordan Rivera"} ) person = response.json().get("person", {}) print(f"Created person {person.get('name')} -> {person.get('uuid')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/person/createPerson', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ name: 'Jordan Rivera' }) }); const { person } = await response.json(); console.log(`Created person ${person.name} -> ${person.uuid}`); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/person/createPerson \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"name": "Jordan Rivera"}' ``` Display name for the person. The response returns a `person` object: Unique identifier for the person. Display name. Optional email associated with the person. Organization the person belongs to. When the person record was created. When the person record was last updated. ### Upload a face image Enroll a face by uploading a `.jpg` or `.png` image (maximum **5 MB**) as `multipart/form-data`. The file must be sent under the form field name `file`. Two optional query parameters control the transaction: * `transaction` — a transaction ID you supply to track and later poll the upload. If omitted, the server generates one and returns it. * `createPersonIfNotFound` — set to `true` to have the system create a new person automatically when the uploaded face does not match an existing one. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY" # Do NOT set Content-Type manually; requests sets the multipart boundary } with open("jordan_rivera.jpg", "rb") as f: files = {"file": ("jordan_rivera.jpg", f, "image/jpeg")} response = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers", headers=headers, params={"transaction": "enroll-jordan-001", "createPersonIfNotFound": "true"}, files=files ) data = response.json() print(f"Transaction: {data.get('transactionId')}") for result in data.get("fileUploadResults", []): print(f" {result.get('fileName')}: success={result.get('success')} {result.get('message', '')}") ``` ```javascript JavaScript theme={null} import fs from 'fs'; const form = new FormData(); const buffer = fs.readFileSync('jordan_rivera.jpg'); form.append('file', new Blob([buffer], { type: 'image/jpeg' }), 'jordan_rivera.jpg'); const url = new URL('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers'); url.searchParams.set('transaction', 'enroll-jordan-001'); url.searchParams.set('createPersonIfNotFound', 'true'); const response = await fetch(url, { method: 'POST', headers: { 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: form }); const data = await response.json(); console.log(`Transaction: ${data.transactionId}`); for (const result of data.fileUploadResults || []) { console.log(` ${result.fileName}: success=${result.success} ${result.message || ''}`); } ``` ```bash cURL theme={null} curl -X POST "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers?transaction=enroll-jordan-001&createPersonIfNotFound=true" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -F "file=@jordan_rivera.jpg;type=image/jpeg" ``` Upload one image per request. Send exactly one `file` part, keep it under 5 MB, and use `.jpg` or `.png`. Let your HTTP client set the `Content-Type: multipart/form-data` boundary — do not set it by hand. The response returns: The transaction ID for this upload (echoed back, or generated if you omitted it). Use it to poll processing status. Per-file upload results, each with `fileName`, `success` (boolean), and `message`. ### Poll the upload transaction Face images process asynchronously. Poll the transaction to learn the resulting `faceId` and the `personUuid` the face was enrolled or linked to. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } for _ in range(10): response = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction", headers=headers, json={"transactionId": "enroll-jordan-001"} ) metadata = response.json().get("faceUploadMetadata", []) if metadata and all(m.get("success") is not None for m in metadata): for m in metadata: print(f"faceId={m.get('faceId')} personUuid={m.get('personUuid')} " f"success={m.get('success')} {m.get('errorMsg', '')}") break time.sleep(2) ``` ```javascript JavaScript theme={null} const headers = { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }; for (let i = 0; i < 10; i++) { const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction', { method: 'POST', headers, body: JSON.stringify({ transactionId: 'enroll-jordan-001' }) }); const { faceUploadMetadata = [] } = await response.json(); if (faceUploadMetadata.length && faceUploadMetadata.every(m => m.success != null)) { for (const m of faceUploadMetadata) { console.log(`faceId=${m.faceId} personUuid=${m.personUuid} success=${m.success} ${m.errorMsg || ''}`); } break; } await new Promise(r => setTimeout(r, 2000)); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"transactionId": "enroll-jordan-001"}' ``` Each entry in `faceUploadMetadata` includes: | Field | Type | Description | | ----------------- | ------- | ----------------------------------------------------------- | | `transactionId` | string | The transaction this upload belongs to | | `faceId` | string | Identifier of the enrolled face matchmaker (once processed) | | `personUuid` | string | Person the face was enrolled or linked to | | `success` | boolean | Whether processing succeeded | | `errorMsg` | string | Error detail when `success` is `false` | | `createdAtMillis` | integer | Upload timestamp in epoch milliseconds | | `origS3Key` | string | Storage key for the original uploaded image | Prefer to enroll a person from a face the camera already captured? Use `createFaceMatchmakerFromSighting` with the `faceEventUuid` of an existing face event and the target `personUuid` to turn that sighting into an enrolled template — no upload required. ### Label a person Attach labels (for example, `employee` or `visitor`) to organize people and filter face events later. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel", headers=headers, json={"personUuid": "YOUR_PERSON_UUID", "label": "employee"} ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ personUuid: 'YOUR_PERSON_UUID', label: 'employee' }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"personUuid": "YOUR_PERSON_UUID", "label": "employee"}' ``` Use `removePersonLabel` with the same body to detach a label, and `findPersonLabelsByOrg` (empty body `{}`) to retrieve a `labelsByPerson` map of every person's labels. ### List people and matchmakers List every enrolled person with `findPeopleByOrg`, and list enrolled face templates with `findFaceMatchmakersByOrg` (or `findFaceMatchmakersByPerson` for one person). ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } people = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg", headers=headers, json={} ).json().get("people", []) for person in people: print(f"{person.get('name')} ({person.get('uuid')})") matchmakers = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg", headers=headers, json={} ).json().get("faceMatchmakers", []) for mm in matchmakers: print(f"faceId={mm.get('id')} personUuid={mm.get('personUuid')} uploaded={mm.get('uploaded')}") ``` ```javascript JavaScript theme={null} const headers = { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }; const people = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg', { method: 'POST', headers, body: '{}' })).json(); for (const person of people.people || []) { console.log(`${person.name} (${person.uuid})`); } const matchmakers = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg', { method: 'POST', headers, body: '{}' })).json(); for (const mm of matchmakers.faceMatchmakers || []) { console.log(`faceId=${mm.id} personUuid=${mm.personUuid} uploaded=${mm.uploaded}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` Each face matchmaker includes `id` (the face ID), `personUuid`, `orgUuid`, `uploaded` (boolean), and `createdOn`. ### Update or remove people and faces | Endpoint | Body | Purpose | | ------------------------------------------------------ | --------------------------------------------------------------- | -------------------------------- | | `/api/faceRecognition/person/getPerson` | `{ "personUuid": "..." }` | Fetch one person | | `/api/faceRecognition/person/updatePerson` | `{ "personSelectiveUpdate": { "uuid": "...", "name": "..." } }` | Update name/email (selective) | | `/api/faceRecognition/person/deletePerson` | `{ "personUuid": "..." }` | Delete a person | | `/api/faceRecognition/matchmaker/getFaceMatchmaker` | `{ "faceId": "..." }` | Fetch one enrolled face | | `/api/faceRecognition/matchmaker/deleteFaceMatchmaker` | `{ "faceId": "..." }` | Remove an enrolled face template | To manage retention and deletion of biometric data, use `deletePerson` (removes the person and their enrollment) and `deleteFaceMatchmaker` (removes a single enrolled face). Face events can be removed individually with `deleteFaceEvent` (below). ## Query face-match events When a camera detects a face, it generates a **face event**. Retrieve events with `findFaceEventsByOrg`, which takes a `searchFilter` and a `pageRequest`. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg", headers=headers, json={ "searchFilter": { "hasName": True, "timestampFilter": { "rangeStart": str(one_day_ago_ms), "rangeEnd": str(now_ms) } }, "pageRequest": {"maxPageSize": 50} } ) data = response.json() for event in data.get("faceEvents", []): match = event.get("selectedPersonMatch") or {} print(f"{event.get('faceName')} on {event.get('deviceUuid')} " f"confidence={match.get('confidence')} at {event.get('eventTimestamp')}") # Fetch the next page if lastEvaluatedKey is present next_key = data.get("lastEvaluatedKey") ``` ```javascript JavaScript theme={null} const headers = { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }; const now = Date.now(); const oneDayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg', { method: 'POST', headers, body: JSON.stringify({ searchFilter: { hasName: true, timestampFilter: { rangeStart: String(oneDayAgo), rangeEnd: String(now) } }, pageRequest: { maxPageSize: 50 } }) }); const data = await response.json(); for (const event of data.faceEvents || []) { const match = event.selectedPersonMatch || {}; console.log(`${event.faceName} on ${event.deviceUuid} confidence=${match.confidence} at ${event.eventTimestamp}`); } const nextKey = data.lastEvaluatedKey; ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "searchFilter": { "hasName": true, "timestampFilter": { "rangeStart": "1712620800000", "rangeEnd": "1712707200000" } }, "pageRequest": { "maxPageSize": 50 } }' ``` ### Search filter All `searchFilter` fields are optional. When `timestampFilter` is omitted, the search defaults to the **last 7 days**. Filter to a set of people. Filter by exact person names. Filter by a name substring (minimum 3 characters after trimming). Takes precedence over `faceNames` if both are set. Filter by person labels. Filter to a set of cameras. Filter to a set of locations. Filter by the presence (`true`) or absence (`false`) of a matched person name. Filter by the presence or absence of a face embedding. Time window with `rangeStart` and `rangeEnd`, each a string containing an epoch-millisecond value (inclusive). Defaults to the last 7 days. ### Page request Maximum number of events to return in one page. Pagination cursor. Pass the `lastEvaluatedKey` from the previous response to fetch the next page. Absent when there are no more results. ### Interpreting a face event Each event in `faceEvents` is an object with these fields: Unique identifier for the face event. When the face was detected, in epoch milliseconds. Name of the matched person. Equal to the `name` of `selectedPersonMatch` when a match was selected. UUID of the matched person, if any. Camera that generated the event. Location of the camera. Confidence (0–1) that the detected image is a face. Confidence (0–1) associated with the generated face signature. Whether the event has a face signature. The chosen person match, with `uuid`, `name`, `faceId`, and `confidence` (0–1). Null when no match was selected. The best candidate matches for the face, each with `uuid`, `name`, `faceId`, and `confidence` (0–1). Useful for reviewing near-matches. Storage key for the event image. Storage key for the event thumbnail. Fetch a single event by its UUID with `getFaceEvent` (body `{ "eventUuid": "..." }`). ## Tune matching and correct events ### Read and update the matching threshold The matching threshold controls how strict face matching is. It is a confidence value between `0` and `1` — a higher value requires a closer match (fewer false positives, more misses); a lower value is more permissive. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } # Read current threshold current = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig", headers=headers, json={} ).json() print(f"Current threshold: {current.get('faceMatchConfidenceThreshold')}") # Raise the threshold to reduce false positives updated = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig", headers=headers, json={"faceMatchConfidenceThreshold": 0.8} ).json() print(f"New threshold: {updated.get('faceMatchConfidenceThreshold')}") ``` ```javascript JavaScript theme={null} const headers = { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }; const current = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig', { method: 'POST', headers, body: '{}' })).json(); console.log(`Current threshold: ${current.faceMatchConfidenceThreshold}`); const updated = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig', { method: 'POST', headers, body: JSON.stringify({ faceMatchConfidenceThreshold: 0.8 }) })).json(); console.log(`New threshold: ${updated.faceMatchConfidenceThreshold}`); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"faceMatchConfidenceThreshold": 0.8}' ``` Minimum match confidence (0–1) required to consider a detected face a match. ### Correct a misidentified event Reassign a face event to the correct person (or set a name directly) with `updateFaceEvent`. This is how you fix false matches. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent", headers=headers, json={ "eventUuid": "YOUR_FACE_EVENT_UUID", "personUuid": "CORRECT_PERSON_UUID" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ eventUuid: 'YOUR_FACE_EVENT_UUID', personUuid: 'CORRECT_PERSON_UUID' }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "eventUuid": "YOUR_FACE_EVENT_UUID", "personUuid": "CORRECT_PERSON_UUID" }' ``` The face event to update. UUID of the correct person to associate with the event. Name to associate with the event. Ignored if `personUuid` matches one of the event's top person matches. To remove an event entirely, call `deleteFaceEvent` with `{ "eventUuid": "..." }`. ## People-counting analytics People counting is aggregate camera analytics — it counts people without identifying them, served by the Report Webservice. Use it for footfall, occupancy, and trend dashboards. ### Count time series `getCountReportV2` returns a time series of counts bucketed by interval. For people counting, set `types` to `["PEOPLE"]`. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) week_ago_ms = now_ms - (7 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/report/getCountReportV2", headers=headers, json={ "startTimeMs": week_ago_ms, "endTimeMs": now_ms, "interval": "DAILY", "scope": "DEVICE", "types": ["PEOPLE"], "uuid": "YOUR_CAMERA_UUID", "timeZone": "America/Los_Angeles" } ) data = response.json() for point in data.get("timeSeriesDataPoints", []): counts = point.get("eventCountMap", {}) print(f"{point.get('dateLocal')}: {counts.get('PEOPLE', 0)} people") ``` ```javascript JavaScript theme={null} const now = Date.now(); const weekAgo = now - (7 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/report/getCountReportV2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ startTimeMs: weekAgo, endTimeMs: now, interval: 'DAILY', scope: 'DEVICE', types: ['PEOPLE'], uuid: 'YOUR_CAMERA_UUID', timeZone: 'America/Los_Angeles' }) }); const data = await response.json(); for (const point of data.timeSeriesDataPoints || []) { const counts = point.eventCountMap || {}; console.log(`${point.dateLocal}: ${counts.PEOPLE || 0} people`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getCountReportV2 \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "startTimeMs": 1712016000000, "endTimeMs": 1712620800000, "interval": "DAILY", "scope": "DEVICE", "types": ["PEOPLE"], "uuid": "YOUR_CAMERA_UUID", "timeZone": "America/Los_Angeles" }' ``` Start of the range in epoch milliseconds. End of the range in epoch milliseconds. Bucket size. One of `MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`. Aggregation scope. One of `REGION`, `DEVICE`, `LOCATION`, `ORG`. Report types to include. Use `["PEOPLE"]` for people counting. Target UUID for the chosen scope (for example, a camera UUID when `scope` is `DEVICE`). Omit for `ORG` scope. IANA time zone used to bucket the data (for example, `America/Los_Angeles`). Each entry in `timeSeriesDataPoints` includes: Bucket start in UTC. Bucket start in the requested time zone. Map of report type to count for the bucket (for example, `{ "PEOPLE": 42 }`). Map describing which devices reported into the bucket. `getCountReportV2` supports many `types` beyond `PEOPLE` — see the [Reports & Analytics](/implementations/reports-analytics) guide for the full report model, additional analytics endpoints, and CSV export. ### Most recent people counts `getMostRecentPeopleCountEvents` returns the latest raw people-count events for a camera — useful for a live headcount tile. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents", headers=headers, json={"deviceUuid": "YOUR_CAMERA_UUID", "numMostRecent": 10} ) for event in response.json().get("events", []): print(f"{event.get('eventTimestamp')}: {event.get('peopleCount')} people") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ deviceUuid: 'YOUR_CAMERA_UUID', numMostRecent: 10 }) }); const data = await response.json(); for (const event of data.events || []) { console.log(`${event.eventTimestamp}: ${event.peopleCount} people`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"deviceUuid": "YOUR_CAMERA_UUID", "numMostRecent": 10}' ``` Camera to read people counts from. Number of most recent count events to return. Each event includes `eventTimestamp` (epoch ms), `peopleCount`, `deviceUuid`, `locationUuid`, and `uuid`. ### Occupancy counts over time `getOccupancyCountsV2` returns a time series of camera-derived occupancy counts for a single camera. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/report/getOccupancyCountsV2", headers=headers, json={ "deviceUuid": "YOUR_CAMERA_UUID", "startTimeMs": day_ago_ms, "endTimeMs": now_ms, "interval": "HOURLY" } ) for point in response.json().get("timeSeriesDataPoints", []): print(f"{point.get('dateLocal')}: {point.get('eventCountMap')}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const dayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/report/getOccupancyCountsV2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ deviceUuid: 'YOUR_CAMERA_UUID', startTimeMs: dayAgo, endTimeMs: now, interval: 'HOURLY' }) }); const data = await response.json(); for (const point of data.timeSeriesDataPoints || []) { console.log(`${point.dateLocal}:`, point.eventCountMap); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getOccupancyCountsV2 \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "deviceUuid": "YOUR_CAMERA_UUID", "startTimeMs": 1712534400000, "endTimeMs": 1712620800000, "interval": "HOURLY" }' ``` Camera to read occupancy counts from. Start of the range in epoch milliseconds. End of the range in epoch milliseconds. Bucket size (`MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`). Each data point adds `timestampMs` and `approximateTimestampMsMap` to the standard `dateUtc`, `dateLocal`, `eventCountMap`, and `reportingDevicesMap` fields. This occupancy report is **camera-derived**. For room presence measured by physical Bluetooth/motion occupancy sensors, see the [IoT Sensors](/implementations/iot-sensors) guide — that is a separate hardware product with its own API. ## Reference: report enums | Enum | Values | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | Report type | `CROWD`, `PEOPLE`, `FACES`, `MOTION`, `BANDWIDTH`, `VEHICLES`, `LICENSEPLATES`, `ALERTS`, `AM_VERIFICATION`, `DWELL` | | Interval | `MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY` | | Scope | `REGION`, `DEVICE`, `LOCATION`, `ORG` | ## Use cases * **Watchlist alerting** — enroll people of interest, then poll `findFaceEventsByOrg` filtered by `personUuids` or `labels` to flag sightings. * **Access reconciliation** — cross-reference face events with access-control activity to confirm the person who badged in matches who the camera saw. * **Model quality loop** — review `topPersonMatches` on low-confidence events, correct them with `updateFaceEvent`, and tune `faceMatchConfidenceThreshold` to balance false positives against misses. * **Footfall dashboards** — chart `getCountReportV2` with `types: ["PEOPLE"]` for daily or hourly traffic trends per camera or location. * **Live occupancy tiles** — surface `getMostRecentPeopleCountEvents` for a near-real-time headcount display. ## Troubleshooting Face Recognition is a licensed, gated feature. Confirm your organization is entitled and that your API key has face-management permissions. Contact your Rhombus account team if the endpoints are not available. Send exactly one file per request under the form field name `file`, keep it under 5 MB, and use `.jpg` or `.png`. Let your HTTP client set the multipart `Content-Type` boundary — do not set `Content-Type` manually. Uploads process asynchronously. Poll the transaction until each entry reports a `success` value. When `success` is `false`, read `errorMsg` for the reason. If you omit `timestampFilter`, the search only covers the last 7 days. Widen the window with `rangeStart`/`rangeEnd` (epoch-millisecond strings), and loosen filters such as `hasName` or `personUuids`. Remember to page with `lastEvaluatedKey`. Adjust `faceMatchConfidenceThreshold` with `updateFaceMatchingConfig`. Raise it (closer to 1) to reduce false positives; lower it to catch more potential matches. Correct individual mistakes with `updateFaceEvent`. ## Next Steps Full count-report model, threshold-crossing counts, and CSV export Detect and search license plates and vehicles across your cameras Physical occupancy, climate, and environmental sensors Browse and test every Rhombus API endpoint # IoT Sensors Source: https://api-docs.rhombus.community/implementations/iot-sensors Read climate data from Rhombus IoT sensors via the API to monitor temperature, humidity, air quality, and environmental conditions across your facilities. ## Overview Rhombus IoT sensors (E-series) continuously monitor environmental conditions across your facilities. These sensors capture temperature, humidity, indoor air quality (IAQ), CO2 levels, TVOC, and PM2.5, reporting data through environmental gateways back to the Rhombus platform. Through the API, you can: * **List sensors** and retrieve their current readings * **Query historical data** for trend analysis and reporting * **Configure sensors** by updating descriptions, locations, and camera associations * **Set up climate alert policies** that trigger when readings cross defined thresholds * **Monitor gateways** that relay data from sensors to the cloud * **Export data** as CSV for external analysis tools ## Prerequisites Before you begin, make sure you have: * A **Rhombus API key** with sensor permissions (generated in the Rhombus Console under Settings > API) * At least one **E-series environmental sensor** deployed and connected to an environmental gateway * A **configured location** where your sensors are installed ## List All Sensors Retrieve the current state of every climate sensor in your organization. This returns each sensor's UUID, name, current readings, battery level, and health status. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/climate/getMinimalClimateStateList", headers=headers, json={} ) data = response.json() for sensor in data.get("climateStates", []): name = sensor.get("name", "Unnamed") temp_c = sensor.get("temperatureCelcius") humidity = sensor.get("humidity") iaq = sensor.get("iaq") battery = sensor.get("batteryPercent") temp_f = round(temp_c * 9 / 5 + 32, 1) if temp_c else None print(f"{name}: {temp_f}°F, {humidity}% RH, IAQ {iaq}, Battery {battery}%") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/climate/getMinimalClimateStateList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); for (const sensor of data.climateStates || []) { const tempF = sensor.temperatureCelcius ? (sensor.temperatureCelcius * 9 / 5 + 32).toFixed(1) : null; console.log(`${sensor.name}: ${tempF}°F, ${sensor.humidity}% RH, IAQ ${sensor.iaq}, Battery ${sensor.batteryPercent}%`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/climate/getMinimalClimateStateList \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` The response includes a `climateStates` array. Key fields for each sensor: | Field | Type | Description | | -------------------- | ------- | ----------------------------------------- | | `sensorUuid` | string | Unique identifier for the sensor | | `name` | string | Display name (e.g., "Server Room Sensor") | | `temperatureCelcius` | float | Current temperature in Celsius | | `humidity` | float | Relative humidity percentage | | `iaq` | float | Indoor Air Quality index | | `co2` | float | CO2 level in ppm | | `tvoc` | float | Total Volatile Organic Compounds | | `pm25` | float | PM2.5 particulate matter | | `batteryPercent` | integer | Battery level (0-100) | | `health` | string | Sensor health status (`GREEN` or `RED`) | | `locationUuid` | string | Location where the sensor is assigned | Temperature is returned in Celsius. To convert to Fahrenheit: `°F = °C × 9/5 + 32`. ## Read Sensor Data Query historical climate events for a specific sensor over a time range. Each event represents a data point with temperature, humidity, air quality, and other environmental readings. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } # Query the last 24 hours of data now_ms = int(time.time() * 1000) one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/climate/getClimateEventsForSensor", headers=headers, json={ "sensorUuid": "YOUR_SENSOR_UUID", "createdAfterMs": one_day_ago_ms, "createdBeforeMs": now_ms, "limit": 100 } ) data = response.json() for event in data.get("climateEvents", []): temp_c = event.get("temp") humidity = event.get("humidity") timestamp = event.get("timestampMs") temp_f = round(temp_c * 9 / 5 + 32, 1) if temp_c else None print(f"[{timestamp}] {temp_f}°F, {humidity}% RH") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneDayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/climate/getClimateEventsForSensor', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ sensorUuid: 'YOUR_SENSOR_UUID', createdAfterMs: oneDayAgo, createdBeforeMs: now, limit: 100 }) }); const data = await response.json(); for (const event of data.climateEvents || []) { const tempF = event.temp ? (event.temp * 9 / 5 + 32).toFixed(1) : null; console.log(`[${new Date(event.timestampMs).toISOString()}] ${tempF}°F, ${event.humidity}% RH`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/climate/getClimateEventsForSensor \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "sensorUuid": "YOUR_SENSOR_UUID", "createdAfterMs": 1712620800000, "createdBeforeMs": 1712707200000, "limit": 100 }' ``` Each climate event includes these fields: | Field | Type | Description | | --------------- | ------- | -------------------------------------- | | `timestampMs` | integer | Event timestamp in epoch milliseconds | | `temp` | float | Temperature in Celsius at this reading | | `humidity` | float | Relative humidity percentage | | `iaq` | float | Indoor Air Quality index | | `co2` | float | CO2 concentration | | `tvoc` | float | Total Volatile Organic Compounds | | `pm25` | float | PM2.5 particulate matter | | `heatIndexDegF` | float | Calculated heat index in Fahrenheit | The `limit` parameter caps the number of events returned. If you need more data points than the limit allows, paginate by adjusting the `createdAfterMs` and `createdBeforeMs` window. ## Configure Sensors ### Get Sensor Configuration Retrieve the full configuration for a specific sensor, including its threshold settings and associated devices. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/climate/getConfig", headers=headers, json={ "uuid": "YOUR_SENSOR_UUID" } ) config = response.json().get("config", {}) print(config) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/climate/getConfig', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ uuid: 'YOUR_SENSOR_UUID' }) }); const data = await response.json(); console.log(data.config); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/climate/getConfig \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "uuid": "YOUR_SENSOR_UUID" }' ``` ### Update Sensor Details Update a sensor's description, location assignment, or camera associations. Fields you include with their corresponding `*Updated` flag set to `true` will be modified; all others remain unchanged. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/climate/updateDetails", headers=headers, json={ "uuid": "YOUR_SENSOR_UUID", "description": "Server Room B - Rack 4 temperature and humidity monitor", "descriptionUpdated": True, "locationUuid": "YOUR_LOCATION_UUID", "locationUuidUpdated": True } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/climate/updateDetails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ uuid: 'YOUR_SENSOR_UUID', description: 'Server Room B - Rack 4 temperature and humidity monitor', descriptionUpdated: true, locationUuid: 'YOUR_LOCATION_UUID', locationUuidUpdated: true }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/climate/updateDetails \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "uuid": "YOUR_SENSOR_UUID", "description": "Server Room B - Rack 4 temperature and humidity monitor", "descriptionUpdated": true, "locationUuid": "YOUR_LOCATION_UUID", "locationUuidUpdated": true }' ``` You must set the corresponding `*Updated` flag to `true` for each field you want to change. For example, setting `description` without `descriptionUpdated: true` will have no effect. ## Set Up Climate Alerts Climate policies define threshold rules that trigger alerts when environmental readings go above or below specified values. Create a policy and associate it with sensors to receive notifications. ### Create a Climate Policy This example creates a policy that alerts when temperature exceeds 80°F (26.7°C) or drops below 40°F (4.4°C). ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/policy/createClimatePolicy", headers=headers, json={ "policy": { "name": "Server Room Temperature Alert", "description": "Alert when server room temperature is outside safe operating range", "scheduledTriggers": [ { "triggerSet": [ { "activity": "TEMPERATURE_EXCEEDED_HIGH", "threshold": 26.7 }, { "activity": "TEMPERATURE_EXCEEDED_LOW", "threshold": 4.4 }, { "activity": "HUMIDITY_EXCEEDED_HIGH", "threshold": 60.0 } ] } ] } } ) data = response.json() print(f"Created policy: {data.get('policyUuid')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/policy/createClimatePolicy', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ policy: { name: 'Server Room Temperature Alert', description: 'Alert when server room temperature is outside safe operating range', scheduledTriggers: [ { triggerSet: [ { activity: 'TEMPERATURE_EXCEEDED_HIGH', threshold: 26.7 }, { activity: 'TEMPERATURE_EXCEEDED_LOW', threshold: 4.4 }, { activity: 'HUMIDITY_EXCEEDED_HIGH', threshold: 60.0 } ] } ] } }) }); const data = await response.json(); console.log(`Created policy: ${data.policyUuid}`); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/policy/createClimatePolicy \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "policy": { "name": "Server Room Temperature Alert", "description": "Alert when server room temperature is outside safe operating range", "scheduledTriggers": [ { "triggerSet": [ { "activity": "TEMPERATURE_EXCEEDED_HIGH", "threshold": 26.7 }, { "activity": "TEMPERATURE_EXCEEDED_LOW", "threshold": 4.4 }, { "activity": "HUMIDITY_EXCEEDED_HIGH", "threshold": 60.0 } ] } ] } }' ``` Threshold values for temperature triggers use Celsius. Convert your target Fahrenheit values before creating the policy: `°C = (°F - 32) × 5/9`. ### List Climate Policies Retrieve all climate policies configured for your organization. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/policy/getClimatePolicies", headers=headers, json={} ) data = response.json() for policy in data.get("policies", []): print(f"{policy.get('name')}: {policy.get('uuid')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/policy/getClimatePolicies', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); for (const policy of data.policies || []) { console.log(`${policy.name}: ${policy.uuid}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/policy/getClimatePolicies \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` ## Environmental Gateways Environmental gateways are the hardware hubs that receive data from nearby E-series sensors via Bluetooth and relay it to the Rhombus cloud. Each gateway supports multiple sensors and connects over your network. Use the gateway status endpoint to check connectivity, firmware versions, and which sensors each gateway is serving. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/climate/getMinimalEnvironmentalGatewayStates", headers=headers, json={} ) data = response.json() for gw in data.get("minimalEnvironmentalGatewayStates", []): print(f"Gateway: {gw.get('name')} ({gw.get('deviceUuid')})") print(f" Health: {gw.get('healthStatus')}, Firmware: {gw.get('firmwareVersion')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/climate/getMinimalEnvironmentalGatewayStates', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); for (const gw of data.minimalEnvironmentalGatewayStates || []) { console.log(`Gateway: ${gw.name} (${gw.deviceUuid})`); console.log(` Health: ${gw.healthStatus}, Firmware: ${gw.firmwareVersion}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/climate/getMinimalEnvironmentalGatewayStates \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` ### Get Gateway Events Query historical events for a specific gateway, such as connectivity changes and sensor registration. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) one_week_ago_ms = now_ms - (7 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/climate/getEventsForEnvironmentalGateway", headers=headers, json={ "deviceUuid": "YOUR_GATEWAY_UUID", "createdAfterMs": one_week_ago_ms, "createdBeforeMs": now_ms } ) print(response.json()) ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/climate/getEventsForEnvironmentalGateway', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ deviceUuid: 'YOUR_GATEWAY_UUID', createdAfterMs: oneWeekAgo, createdBeforeMs: now }) }); const data = await response.json(); console.log(data); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/climate/getEventsForEnvironmentalGateway \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "deviceUuid": "YOUR_GATEWAY_UUID", "createdAfterMs": 1712016000000, "createdBeforeMs": 1712620800000 }' ``` ## Export Sensor Data Export climate sensor data as a CSV file for use in spreadsheets, data analysis tools, or compliance reporting. Specify a sensor and time range to download all recorded readings. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } # Export the last 30 days of data now_ms = int(time.time() * 1000) thirty_days_ago_ms = now_ms - (30 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/export/climateEvents", headers=headers, json={ "sensorUuid": "YOUR_SENSOR_UUID", "createdAfterMs": thirty_days_ago_ms, "createdBeforeMs": now_ms } ) # Response is CSV data with open("climate_export.csv", "w") as f: f.write(response.text) print("Exported climate data to climate_export.csv") ``` ```javascript JavaScript theme={null} const now = Date.now(); const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/export/climateEvents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ sensorUuid: 'YOUR_SENSOR_UUID', createdAfterMs: thirtyDaysAgo, createdBeforeMs: now }) }); // Response is CSV text const csvData = await response.text(); console.log(csvData); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/export/climateEvents \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "sensorUuid": "YOUR_SENSOR_UUID", "createdAfterMs": 1710028800000, "createdBeforeMs": 1712620800000 }' \ -o climate_export.csv ``` The export endpoint returns CSV data directly in the response body, not JSON. Use the `-o` flag in cURL or write the response text to a file in your application. You can also export environmental gateway events with the same pattern using `/api/export/environmentalGatewayEvents`. ## Next Steps Set up automated alert responses when sensor thresholds trigger alarms Receive real-time push notifications when sensor readings cross thresholds # LAN Realtime Detection Overlay Source: https://api-docs.rhombus.community/implementations/lan-realtime-detection-overlay Parse AI bounding boxes embedded in the LAN H.264 WebSocket stream and render them on top of live video using the Rhombus React SDK or your own client. ## Overview The LAN agent's WebSocket video stream embeds AI detection results directly into the H.264 encapsulation header. When the on-camera inference pipeline produces a new detection, it is spliced as a TLV field into the next outgoing video frame on the same WebSocket — no separate detection channel. This guide covers: * The TLV encapsulation format and the JSON schema inside the `AI_DETECTIONS` field * Connecting to the live LAN H.264 WebSocket and reading both binary frames and the text init message * Drawing detection boxes on top of [`RhombusRealtimePlayer`](/implementations/react-sdk) using a parallel detection-only WebSocket * A from-scratch parser reference for non-React consumers If you only need a player, embed [`RhombusRealtimePlayer`](/implementations/react-sdk) — it handles auth, WebCodecs decoding, and resolution negotiation. This guide is for adding a detection-overlay layer on top, or for clients that don't use the React SDK. ## Connecting to the LAN realtime stream ### Get the WebSocket URL Call `POST /api/camera/getMediaUris` and read: * `lanLiveH264Uris` (array of strings) — LAN URLs, when the client and camera share a network * `wanLiveH264Uri` (string) — WAN URL, routed through Rhombus For the lower-resolution variant, swap `/ws` for `/wsl` in the path. ### Authenticate Both modes use a federated session token minted on your backend via `POST /api/org/generateFederatedSessionToken`. Never put your API key in browser code. | Mode | Auth method | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | WAN | Append `?x-auth-scheme=federated-token&x-auth-ft=` to the URL before opening the WebSocket. | | LAN | Append `?x-auth-scheme=federated-token&x-auth-ft=` to the URL the same way as WAN. (Earlier SDK versions used an `RFT` cookie; that was removed in v1.0 and LAN now uses URL query parameters.) | The full token-minting backend example (Express, FastAPI, Next.js) lives in the [React SDK guide](/implementations/react-sdk#backend-setup) — reuse it. ## What the server sends Immediately after the WebSocket upgrade and before any binary frames, the server sends a single **text** message describing the stream: ```json theme={null} {"action":"init","width":1920,"height":1080,"codec":"h264","framerate":15} ``` Read the dimensions if your renderer needs the source resolution. Bounding boxes are resolution-independent (permyriad units), so most overlays don't need this. After the init message, every subsequent message is a **binary** frame containing the TLV-encoded encapsulation header followed by raw H.264 NAL data. ## Encapsulation header (TLV format) Each binary message contains a sequence of TLVs. Every TLV uses the same wire format: ```text theme={null} [1 byte type] [3 bytes length, big-endian] [N bytes value] ``` ### TLV types | Type | Name | Value | Notes | | -----: | ---------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- | | `0x00` | `SPS_PPS_IFRAME` | H.264 NAL data | Keyframe (SPS/PPS/I-frame). Always last TLV in the message. | | `0x01` | `NON_IFRAME` | H.264 NAL data | Delta frame (P/B). Always last TLV in the message. | | `0x02` | `TIMESTAMP` | 8-byte uint64 BE | Server wall-clock time in **milliseconds**. | | `0x03` | `PTS_US` | 8-byte uint64 BE | Third-party PTS in **microseconds**. Optional; used for B-frame reordering. | | `0x04` | `AI_DETECTIONS` | UTF-8 JSON string | New AI detections. Present only when a new inference result is available. Not null-terminated — use the length field. | ### Wire layout ```text theme={null} ┌──────────────────────────────────────────────────┐ │ TIMESTAMP (0x02): 4 + 8 = 12 bytes │ always present ├──────────────────────────────────────────────────┤ │ PTS_US (0x03): 4 + 8 = 12 bytes │ optional ├──────────────────────────────────────────────────┤ │ AI_DETECTIONS (0x04): 4 + N bytes │ only when a new │ │ detection is available ├──────────────────────────────────────────────────┤ │ frame-data (0x00 or 0x01): 4 + N bytes │ always last; │ │ value is raw H.264 └──────────────────────────────────────────────────┘ ``` The frame-data TLV (`0x00` or `0x01`) is **always the last entry** — the LAN agent's encoder explicitly inserts metadata TLVs ahead of the frame entry. A safe parser stops walking TLVs once it encounters a frame-data type. ## Parsing the encapsulation header Walk TLV fields until you hit type `0x00` or `0x01` (the frame-data entry): ```typescript theme={null} type ParsedFrame = { timestampMs: number | null; ptsUs: number | null; detectionJson: string | null; isKeyframe: boolean; h264Data: Uint8Array; }; export function parseEncapHeader(buffer: ArrayBuffer): ParsedFrame { const view = new DataView(buffer); const bytes = new Uint8Array(buffer); let offset = 0; let timestampMs: number | null = null; let ptsUs: number | null = null; let detectionJson: string | null = null; while (offset + 4 <= buffer.byteLength) { const type = view.getUint8(offset); const len = (view.getUint8(offset + 1) << 16) | (view.getUint8(offset + 2) << 8) | view.getUint8(offset + 3); const valueStart = offset + 4; if (type === 0x00 || type === 0x01) { return { timestampMs, ptsUs, detectionJson, isKeyframe: type === 0x00, h264Data: bytes.subarray(valueStart, valueStart + len), }; } if (type === 0x02) { const hi = view.getUint32(valueStart); const lo = view.getUint32(valueStart + 4); timestampMs = hi * 0x100000000 + lo; } else if (type === 0x03) { const hi = view.getUint32(valueStart); const lo = view.getUint32(valueStart + 4); ptsUs = hi * 0x100000000 + lo; } else if (type === 0x04) { detectionJson = new TextDecoder().decode( bytes.subarray(valueStart, valueStart + len) ); } // Unknown types are skipped silently. offset = valueStart + len; } throw new Error("Encapsulation header missing frame-data TLV"); } ``` The Rhombus React SDK uses an equivalent parser at [`parseRhombusH264Binary.ts`](https://github.com/RhombusSystems/rhombus-react-sdk/blob/main/src/stream/parseRhombusH264Binary.ts) — the canonical client-side reference. ## Detection JSON schema `AI_DETECTIONS` carries a JSON array of detection objects. The detection objects carry no timestamp of their own — every detection in a message was analyzed from the same frame, so use the enclosing frame's `TIMESTAMP` TLV (type `0x02`) as the analysis time. ### Required fields | Field | Type | Units | Description | | ----- | ------- | ------------------- | ------------------------------------------------------------------------------------------------------- | | `t` | int | enum | Detection type. `0` Human, `1` Vehicle, `2` Face, `3` License Plate (LPR), `4` Pose, `5` CLIP Embedding | | `c` | int | permyriad (0–10000) | Confidence. Divide by 100 for percent. | | `id` | int | — | Tracker object id. Stable across frames for the same tracked object. | | `b` | int\[4] | permyriad | Bounding box `[left, top, right, bottom]` | | `rs` | float | seconds | Relative-second timestamp within the detection window | ### Optional fields | Field | Type | Notes | | ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clr` | object | Color histogram. Keys are color names (e.g. `"red"`, `"blue"`); values are permyriad. | | `tight_crop_xxyy` | int\[4] | Tight bbox within the detection's crop window: `[x_min, x_max, y_min, y_max]` (permyriad). Useful when the consumer wants a tighter box than the padded detection window. | | `ec` | int | Embedding confidence (permyriad), present when an embedding is computed. | | `et` | string | Embedding type identifier. | | `e` | string | Embedding vector (string-encoded; length depends on type). | | `il` | string | Image-locator reference for the detection's crop. | ### Example ```json theme={null} [ { "t": 0, "c": 8500, "id": 3, "b": [1200, 3400, 4500, 8900], "rs": 2.5, "clr": {"red": 4000, "blue": 6000}, "tight_crop_xxyy": [1500, 4200, 3700, 8500] } ] ``` **Forward-compatible parsing.** Future firmware releases will add LPR text (`lp_chars`, `lp_confidence`), pose skeletons (`pose_permyriad_points` — 38-joint, **not** the 17-joint COCO set), and re-identification embeddings. Treat all unrecognized fields as optional and ignore unknown keys, so your client keeps working when those fields land. ## Drawing bounding boxes on a canvas Bounding box coordinates are permyriad (0–10000) and resolution-independent. Convert to pixels using the canvas dimensions: ```javascript theme={null} const TYPE_COLORS = { 0: "#00ff00", // Human — green 1: "#0088ff", // Vehicle — blue 2: "#ff00ff", // Face — magenta 3: "#ffff00", // LPR — yellow 4: "#00ffff", // Pose — cyan 5: "#ff8800", // CLIP — orange }; const TYPE_LABELS = ["Human", "Vehicle", "Face", "LPR", "Pose", "CLIP"]; export function drawDetections(ctx, canvasWidth, canvasHeight, detections) { ctx.clearRect(0, 0, canvasWidth, canvasHeight); for (const det of detections) { const [left, top, right, bottom] = det.b; const x = (left / 10000) * canvasWidth; const y = (top / 10000) * canvasHeight; const w = ((right - left) / 10000) * canvasWidth; const h = ((bottom - top) / 10000) * canvasHeight; ctx.strokeStyle = TYPE_COLORS[det.t] ?? "#ffffff"; ctx.lineWidth = 2; ctx.strokeRect(x, y, w, h); const conf = Math.round(det.c / 100); const label = `${TYPE_LABELS[det.t] ?? "Unknown"} ${conf}% #${det.id}`; ctx.fillStyle = ctx.strokeStyle; ctx.font = "12px monospace"; ctx.fillText(label, x, Math.max(10, y - 4)); } } ``` For a 1280×720 canvas and `b: [1200, 3400, 4500, 8900]`, this yields `(x=153.6, y=244.8, w=422.4, h=396.0)`. ## Timing behavior * **Detections are not present on every frame.** The AI pipeline analyzes a subset of frames (typically 2–10 fps). Most frames carry no `AI_DETECTIONS` TLV. * **Detections carry no per-detection timestamp.** Align each detection set on the enclosing frame's `TIMESTAMP` TLV (type `0x02`) — the server wall-clock millisecond stamp the encoder writes for that frame. Because the on-camera inference pipeline and the encoder run independently, a detection rides whatever frame happens to leave the encoder next and may correspond to a frame captured slightly earlier; the stream does not expose that offset, so the carrier frame's `TIMESTAMP` is the anchor to use, especially for VOD or buffered playback. * **Persist between updates.** To keep boxes visible between detection updates, hold the most recent set and keep redrawing it until a newer set arrives or a TTL elapses. A 2-second TTL is a safe default. ## Extending `RhombusRealtimePlayer` with detection rendering The React SDK's `RhombusRealtimePlayer` doesn't currently surface AI detections to the host application. Until it does, the simplest pattern is to open a **second WebSocket** to the same URL purely to read AI\_DETECTIONS, and draw the result on a `` overlaid on the player. A parallel WebSocket doubles the egress for that camera. Use it only on the page that needs detections, and close it on unmount. ```tsx theme={null} import { RhombusRealtimePlayer } from "@rhombussystems/react"; import { useEffect, useRef, useState } from "react"; import { parseEncapHeader } from "./parseEncapHeader"; // from earlier in this guide import { drawDetections } from "./drawDetections"; // from earlier in this guide type Props = { cameraUuid: string; /** WebSocket URL for this camera (from getMediaUris + auth append) */ detectionWsUrl: string; /** Optional: how long to keep boxes after the last update */ detectionTtlMs?: number; }; export function RhombusRealtimePlayerWithDetections({ cameraUuid, detectionWsUrl, detectionTtlMs = 2000, }: Props) { const canvasRef = useRef(null); const detectionsRef = useRef([]); const lastTsRef = useRef(0); const [size, setSize] = useState({ width: 1920, height: 1080 }); useEffect(() => { const ws = new WebSocket(detectionWsUrl); ws.binaryType = "arraybuffer"; ws.onmessage = (event) => { // The server sends one text init message before any binary frames. if (typeof event.data === "string") { try { const init = JSON.parse(event.data); if (init.action === "init" && init.width && init.height) { setSize({ width: init.width, height: init.height }); } } catch { // Ignore non-JSON text messages. } return; } try { const { detectionJson } = parseEncapHeader(event.data); if (!detectionJson) return; const dets = JSON.parse(detectionJson); detectionsRef.current = dets; lastTsRef.current = Date.now(); } catch (err) { console.warn("Failed to parse encap header", err); } }; return () => ws.close(); }, [detectionWsUrl]); useEffect(() => { let raf = 0; const tick = () => { const canvas = canvasRef.current; if (canvas) { const ctx = canvas.getContext("2d"); if (ctx) { const fresh = Date.now() - lastTsRef.current < detectionTtlMs; drawDetections( ctx, canvas.width, canvas.height, fresh ? detectionsRef.current : [] ); } } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [detectionTtlMs]); return (
); } ``` Resolve `detectionWsUrl` on your backend the same way the SDK does: call `getMediaUris`, pick the appropriate `wanLiveH264Uri` or LAN entry, then append `?x-auth-scheme=federated-token&x-auth-ft=` (both WAN and LAN) before passing the URL to the browser. ## From-scratch parser reference For non-React clients (a vanilla web page, Node, Electron), the same parser drives a minimal overlay. Decoding the H.264 itself requires WebCodecs (browser) or ffmpeg/libav (Node) and is out of scope, but reading detections from the WebSocket needs only the parser above: ```html theme={null} ``` ## HTTP streams vs WebSocket The HTTP `video/h264` stream variant strips the encapsulation header entirely and delivers only raw H.264 NAL data. **Detections ride only on the WebSocket transport.** Use the WebSocket URLs from `getMediaUris` (`lanLiveH264Uris` / `wanLiveH264Uri`) for any flow that needs detections. ## Troubleshooting **Boxes appear in the wrong location** Bbox coordinates are permyriad (0–10000), not pixels and not 0–1. Make sure the renderer divides by 10000 before multiplying by the canvas dimensions. **Boxes appear to lag the video** Detections have no timestamp of their own — align each set on the enclosing frame's `TIMESTAMP` TLV (type `0x02`). The detection rides whatever frame leaves the encoder next, so it can trail the analyzed frame slightly; the carrier frame's `TIMESTAMP` is the only timing anchor the stream provides. **Boxes vanish for a few hundred milliseconds, then reappear** The AI pipeline produces results at 2–10 fps and detections do not ride every video frame. Persist the most-recent detection set with a TTL (e.g. 2 s) so the overlay stays stable between updates. **Receiver only ever gets binary frames; never sees the init message** Confirm your WebSocket handler accepts text frames before binary frames. The init is a single text message sent once per connection. **LAN auth fails locally** LAN auth is appended to the WebSocket URL as query parameters (the same as WAN), so it works from any origin including `localhost`. If LAN fails, the usual cause is network reachability: the browser must reach the camera's LAN host directly (routing, firewall, and HTTPS-vs-HTTP mixed-content rules apply). If the LAN host is unreachable, connect via WAN instead, or proxy through your backend. ## Next Steps Drop-in `RhombusRealtimePlayer` and `RhombusBufferedPlayer` components. HLS, shared streams, thumbnails, and frame capture. # License Plate Recognition & Vehicles Source: https://api-docs.rhombus.community/implementations/lpr-vehicle Query license plate detection events, search plates with fuzzy matching, build a watchlist of saved vehicles, run per-device reports, and export vehicle data as CSV with the Rhombus API. ## Overview Rhombus cameras with License Plate Recognition (LPR) detect vehicles and read their license plates as they pass through a scene. Each detection becomes a vehicle event that records the plate, the reporting camera and location, a captured image, and the time it was seen. Through the API, you can: * **Query detection events** across your organization, filtered by device, location, plate, name, or label * **Search a specific plate** with exact or fuzzy matching to find every time it was seen * **Build a watchlist** of saved vehicles ("plates of interest") with alert and trust flags, names, and labels * **Run per-device reports** that bucket detections by hour, day, week, or month * **Export vehicle events** as CSV for external analysis or record keeping Every vehicle event timestamp is reported in **epoch milliseconds**, but the field name differs by endpoint (`startTimeMs`, `startTime`, `timestampMs`, `createdAtMillis`, `eventTimestamp`). Each section below calls out the exact field to use. There is no region, state, or numeric confidence field on vehicle events — do not expect one. ## Prerequisites Before you begin, make sure you have: * A **Rhombus API key** with read access to cameras (generated in the Rhombus Console under Settings > API). Reporting a misread event additionally requires the **`MANAGE_LICENSEPLATES`** permission, and CSV export requires the **`REPORT_ADMINISTRATION`** permission. * At least one **LPR-capable camera** deployed and reading plates at a location * The **device UUID** of that camera, which you can retrieve from the camera endpoints or the Rhombus Console All requests are `POST` to `https://api2.rhombussystems.com` and authenticate with the `x-auth-scheme` and `x-auth-apikey` headers shown in every example. ## Workflow 1: Query recent detections Use `getVehicleEvents` to retrieve detection events across your organization. This is the primary endpoint for pulling LPR activity into a dashboard or report. The request combines two kinds of criteria that behave differently: * **Filter fields** (`deviceUuidFilter`, `locationUuidFilter`) are **ANDed** — a returned event must satisfy *all* filters you provide. * **Query fields** (`nameQuery`, `licensePlateExactQuery`, `vehicleLabelQuery`, `licensePlateFuzzyQuery`, `unnamedQuery`) are **ORed** — a returned event must satisfy *at least one* query you provide. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } # Query the last 24 hours of detections from one camera now_ms = int(time.time() * 1000) one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/vehicle/getVehicleEvents", headers=headers, json={ "startTimeMs": one_day_ago_ms, "endTimeMs": now_ms, "deviceUuidFilter": ["YOUR_CAMERA_UUID"] } ) data = response.json() for event in data.get("events", []): plate = event.get("vehicleLicensePlate") ts = event.get("eventTimestamp") device = event.get("deviceUuid") print(f"[{ts}] {plate} seen by {device}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneDayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/vehicle/getVehicleEvents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ startTimeMs: oneDayAgo, endTimeMs: now, deviceUuidFilter: ['YOUR_CAMERA_UUID'] }) }); const data = await response.json(); for (const event of data.events || []) { console.log(`[${new Date(event.eventTimestamp).toISOString()}] ${event.vehicleLicensePlate} seen by ${event.deviceUuid}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/getVehicleEvents \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "startTimeMs": 1712620800000, "endTimeMs": 1712707200000, "deviceUuidFilter": ["YOUR_CAMERA_UUID"] }' ``` ### Request fields Start of the time window (oldest) in epoch milliseconds. End of the time window (newest) in epoch milliseconds. Return only events from these camera UUIDs. ANDed with other filters. Return only events from these location UUIDs. ANDed with other filters. Return events whose vehicle name matches one of these values. ORed with other queries. Return events whose plate exactly matches one of these values. ORed with other queries. Return events tagged with one of these vehicle labels. ORed with other queries. Return events whose plate approximately matches this partial or complete plate, allowing for character misreads. ORed with other queries. If `false`, return only events that have a name; if `true`, return only events without a name. Omit if not needed. ORed with other queries. ### Response fields The response contains an `events` array. Each event has these fields: Unique identifier for the vehicle event. Organization the event belongs to. UUID of the camera that reported the detection. Location where the detection occurred. The recognized license plate string. Plate strings considered full matches for this detection. Plate strings considered partial matches for this detection. S3 key for the full detection image. This is a storage key, not a URL. S3 key for the detection thumbnail. This is a storage key, not a URL. When the detection occurred, in epoch milliseconds. Vehicle name, if the plate is associated with a saved vehicle. `imageS3Key` and `thumbnailS3Key` are S3 storage keys, not directly viewable image URLs. Use the media and footage endpoints to retrieve the image bytes. The `getRecentVehicleEvents`, `getRecentVehicleEventsByLocation`, and `getRecentVehicleEventsForVehicle` endpoints are legacy. Prefer `getVehicleEvents` for new integrations — its filter/query model supersedes them. ## Workflow 2: Search a plate with fuzzy matching When you have a specific plate to track down, `searchLicensePlates` finds every event that matches it. Enable `fuzzy` to tolerate character misreads — helpful when a plate is partially obscured or the original read was imperfect. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) one_week_ago_ms = now_ms - (7 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/search/searchLicensePlates", headers=headers, json={ "licensePlate": "ABC123", "startTime": one_week_ago_ms, "endTime": now_ms, "fuzzy": True } ) data = response.json() for hit in data.get("vehicleEvents", []): plate = hit.get("vehicleLicensePlate") matched = hit.get("searchMatchedTerm") match_type = hit.get("searchMatchedType") print(f"{plate} matched '{matched}' ({match_type}) at {hit.get('eventTimestamp')}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/search/searchLicensePlates', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ licensePlate: 'ABC123', startTime: oneWeekAgo, endTime: now, fuzzy: true }) }); const data = await response.json(); for (const hit of data.vehicleEvents || []) { console.log(`${hit.vehicleLicensePlate} matched '${hit.searchMatchedTerm}' (${hit.searchMatchedType})`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/search/searchLicensePlates \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "licensePlate": "ABC123", "startTime": 1712102400000, "endTime": 1712707200000, "fuzzy": true }' ``` ### Request fields The plate number to search for. Start of the search window in epoch milliseconds. Note the field name is `startTime`, not `startTimeMs`. End of the search window in epoch milliseconds. Limit the search to these camera UUIDs. When `true`, allows approximate matching for misread characters. The `forcedFuzziness` field is deprecated. Use the boolean `fuzzy` flag instead. ### Response fields The `vehicleEvents` array contains all the [vehicle event fields](#response-fields) from Workflow 1, plus two search-specific fields: The plate term that matched your query. How the match was made (for example, exact versus fuzzy). ## Workflow 3: Build a watchlist of saved vehicles A saved vehicle is a "plate of interest" you want to track. Each saved vehicle carries an `alert` flag (notify when seen) and a `trust` flag (treat as known/safe), plus an optional name, description, and labels. Saving a vehicle also lets Rhombus attach the `name` to future detection events for that plate. ### Save a vehicle ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/vehicle/saveVehicle", headers=headers, json={ "vehicleLicensePlate": "ABC123", "createdAtMillis": int(time.time() * 1000), "name": "Delivery Van", "description": "White Ford Transit - approved vendor", "alert": True, "trust": True } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/vehicle/saveVehicle', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ vehicleLicensePlate: 'ABC123', createdAtMillis: Date.now(), name: 'Delivery Van', description: 'White Ford Transit - approved vendor', alert: true, trust: true }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/saveVehicle \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "vehicleLicensePlate": "ABC123", "createdAtMillis": 1712707200000, "name": "Delivery Van", "description": "White Ford Transit - approved vendor", "alert": true, "trust": true }' ``` The plate that identifies this saved vehicle. Creation timestamp in epoch milliseconds. Whether to raise an alert when this vehicle is detected. Whether this vehicle is trusted (known/safe). Display name for the vehicle. Free-text description. S3 key for a thumbnail image of the vehicle. ### List saved vehicles Retrieve every saved vehicle in your organization. The request body is empty. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/vehicle/getVehicles", headers=headers, json={} ) data = response.json() for vehicle in data.get("vehicles", []): print(f"{vehicle.get('licensePlate')}: {vehicle.get('name')} " f"(alert={vehicle.get('alert')}, trust={vehicle.get('trust')})") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/vehicle/getVehicles', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); for (const vehicle of data.vehicles || []) { console.log(`${vehicle.licensePlate}: ${vehicle.name} (alert=${vehicle.alert}, trust=${vehicle.trust})`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/getVehicles \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` Each entry in the `vehicles` array is a saved vehicle: The plate that identifies the saved vehicle. Organization the vehicle belongs to. When the vehicle was saved, in epoch milliseconds. Whether detections raise an alert. Whether the vehicle is trusted. Display name. Free-text description. S3 key for the vehicle thumbnail. ### Add and remove labels Labels group saved vehicles into categories such as "employee," "vendor," or "flagged." Add a label to a plate with `addVehicleLabel` and remove it with `removeVehicleLabel`. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/vehicle/addVehicleLabel", headers=headers, json={ "vehicleLicensePlate": "ABC123", "label": "Approved Vendor" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/vehicle/addVehicleLabel', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ vehicleLicensePlate: 'ABC123', label: 'Approved Vendor' }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/addVehicleLabel \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "vehicleLicensePlate": "ABC123", "label": "Approved Vendor" }' ``` To see every label assigned across your organization, call `getVehicleLabelsForOrg`. Its response contains a `vehicleLabels` map keyed by license plate, where each value is the set of labels on that plate. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/vehicle/getVehicleLabelsForOrg", headers=headers, json={} ) labels = response.json().get("vehicleLabels", {}) for plate, plate_labels in labels.items(): print(f"{plate}: {', '.join(plate_labels)}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/vehicle/getVehicleLabelsForOrg', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({}) }); const data = await response.json(); for (const [plate, plateLabels] of Object.entries(data.vehicleLabels || {})) { console.log(`${plate}: ${plateLabels.join(', ')}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/getVehicleLabelsForOrg \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{}' ``` ### Associate past events with a saved vehicle If a plate was detected before you saved it as a vehicle, use `associateEventsToVehicle` to attach those historical detection events to the saved vehicle. Pass the event UUIDs (from Workflow 1 or 2) and the plate. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/vehicle/associateEventsToVehicle", headers=headers, json={ "vehicleLicensePlate": "ABC123", "eventUuids": ["EVENT_UUID_1", "EVENT_UUID_2"] } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/vehicle/associateEventsToVehicle', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ vehicleLicensePlate: 'ABC123', eventUuids: ['EVENT_UUID_1', 'EVENT_UUID_2'] }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/associateEventsToVehicle \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "vehicleLicensePlate": "ABC123", "eventUuids": ["EVENT_UUID_1", "EVENT_UUID_2"] }' ``` ### Delete a saved vehicle Remove a vehicle from your watchlist by its plate. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/vehicle/deleteVehicle", headers=headers, json={ "vehicleLicensePlate": "ABC123" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/vehicle/deleteVehicle', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ vehicleLicensePlate: 'ABC123' }) }); console.log(await response.json()); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/vehicle/deleteVehicle \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{"vehicleLicensePlate": "ABC123"}' ``` When a detection is misread or attributed to the wrong plate, report it with `reportVehicleEvent` (passing the event's `eventUuid`). This feedback helps improve recognition accuracy. Reporting requires the `MANAGE_LICENSEPLATES` permission on your API key. ## Workflow 4: Per-device reports and CSV export ### Per-device report `getLicensePlatesByDevice` returns license plate detections for a single camera, bucketed by a reporting interval. Provide the anchor time as `timestampMs` (epoch milliseconds) and choose an `interval`. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/report/getLicensePlatesByDevice", headers=headers, json={ "deviceUuid": "YOUR_CAMERA_UUID", "timestampMs": int(time.time() * 1000), "interval": "DAILY" } ) data = response.json() for event in data.get("licensePlateEvents", []): print(f"{event.get('vehicleLicensePlate')} at {event.get('eventTimestamp')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/report/getLicensePlatesByDevice', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ deviceUuid: 'YOUR_CAMERA_UUID', timestampMs: Date.now(), interval: 'DAILY' }) }); const data = await response.json(); for (const event of data.licensePlateEvents || []) { console.log(`${event.vehicleLicensePlate} at ${event.eventTimestamp}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getLicensePlatesByDevice \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "deviceUuid": "YOUR_CAMERA_UUID", "timestampMs": 1712707200000, "interval": "DAILY" }' ``` The camera UUID to report on. Anchor time for the report, as a UNIX timestamp in milliseconds. Reporting bucket. One of `HOURLY`, `DAILY`, `WEEKLY`, or `MONTHLY`. The `dateLocal` field is deprecated. Use `timestampMs` instead. The response `licensePlateEvents` array uses the same [vehicle event fields](#response-fields) described in Workflow 1. ### Export to CSV `export/vehicleEventsV2` streams matching vehicle events as CSV. It accepts the **same request body as `getVehicleEvents`** (Workflow 1) — the filter and query fields, ANDed and ORed the same way — and returns `text/csv` directly in the response body rather than JSON. This endpoint requires the `REPORT_ADMINISTRATION` permission. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) thirty_days_ago_ms = now_ms - (30 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/export/vehicleEventsV2", headers=headers, json={ "startTimeMs": thirty_days_ago_ms, "endTimeMs": now_ms, "deviceUuidFilter": ["YOUR_CAMERA_UUID"] } ) with open("vehicle_events.csv", "w") as f: f.write(response.text) print("Exported vehicle events to vehicle_events.csv") ``` ```javascript JavaScript theme={null} const now = Date.now(); const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/export/vehicleEventsV2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ startTimeMs: thirtyDaysAgo, endTimeMs: now, deviceUuidFilter: ['YOUR_CAMERA_UUID'] }) }); // Response is CSV text, not JSON const csvData = await response.text(); console.log(csvData); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/export/vehicleEventsV2 \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "startTimeMs": 1710115200000, "endTimeMs": 1712707200000, "deviceUuidFilter": ["YOUR_CAMERA_UUID"] }' \ -o vehicle_events.csv ``` `export/vehicleEventsV2` supersedes the older `export/vehicleEvents` endpoint, which is deprecated. Use V2 for new integrations — it accepts the richer `getVehicleEvents` filter/query model. ## Use cases Save your fleet and approved vendor plates with the `trust` flag, then reconcile them against detection events to confirm on-time arrivals. Save plates of interest with `alert: true` and pair detections with webhooks to notify security when a flagged vehicle is seen. Use per-device reports to count daily or weekly vehicle traffic at a gate or lot entrance. Fuzzy-search a partial plate from an incident, then export the matching events as CSV for a report. ## Troubleshooting Confirm you are sending **epoch milliseconds**, not seconds, and that you are using the correct field name for the endpoint: `startTimeMs`/`endTimeMs` for `getVehicleEvents` and `export/vehicleEventsV2`, `startTime`/`endTime` for `searchLicensePlates`, and `timestampMs` for `getLicensePlatesByDevice`. A value in seconds will resolve to 1970 and return nothing. LPR reads can vary by lighting, angle, and speed. Set `fuzzy: true` on `searchLicensePlates` to tolerate character misreads, and check `matchingLicensePlates` and `partialLicensePlates` on returned events for near matches. Remember that query fields (`nameQuery`, `licensePlateExactQuery`, `vehicleLabelQuery`, `licensePlateFuzzyQuery`, `unnamedQuery`) are ORed — an event only has to match one of them. Filter fields (`deviceUuidFilter`, `locationUuidFilter`) are ANDed. Narrow results by moving criteria into filters or reducing the number of queries. `imageS3Key` and `thumbnailS3Key` are S3 storage keys, not URLs you can open directly. Use the Rhombus media and footage endpoints to fetch the image content for a given key. Reporting a misread event requires the `MANAGE_LICENSEPLATES` permission on your API key. CSV export requires `REPORT_ADMINISTRATION`. Generate or update your key with the needed permissions in the Rhombus Console under Settings > API. ## Next Steps Detect and identify people, and count occupancy alongside your vehicle analytics Receive real-time notifications when a watchlisted vehicle is detected Explore complete request and response schemas for every vehicle endpoint # QR Code Access Control Source: https://api-docs.rhombus.community/implementations/qr-code-access-control Implement QR code door unlock with Rhombus access control — generate visitor credentials, validate codes, and trigger touchless door entry via the API. This feature is currently in **beta** and is not fully rolled out to all customers. Features and functionality are subject to change. To request access to this feature, contact your Rhombus representative or post in the [Rhombus Developer Community](https://rhombus.community). ## Overview QR Code Unlock allows authorized users to gain entry by presenting a QR code to a Rhombus security camera or DR40 door controller. The camera recognizes the code, validates it against the Rhombus backend, and unlocks the door instantly when authorized. This implementation provides a fast, secure, and camera-authenticated method for controlled entry without requiring physical keycards, badges, or mobile apps. ## How It Works ### Generate QR Code via API An admin or integrated system generates a secure, time-bound QR code using the Rhombus API. The code is returned as a base64 string that can be converted to an image. ### User Presents QR Code The user displays the QR code on their mobile device or printed material and holds it up to a Rhombus camera assigned to the door. ### Camera Authenticates & Unlocks The camera reads and validates the QR code against the Rhombus backend. If authorized and within the valid time window, the door unlocks automatically. ### Event Logging Every unlock event is logged with visual evidence from the camera, providing a complete audit trail of access attempts. ## Use Cases QR Code Unlock is ideal for various access control scenarios: Grant one-day or time-limited access to visitors without needing to issue physical badges. Dispatch QR codes to technicians or contractors for temporary access to specific areas. Issue tenant-specific codes with customized durations for different access levels. Schedule access during specific delivery hours with time-limited QR codes. ## Prerequisites Before implementing QR Code access control, ensure you have: * An active Rhombus account with API access * A valid API key from the [Rhombus Console](https://console.rhombussystems.com) * At least one Rhombus camera or DR40 door controller configured for access control * The UUID of the access-controlled door you want to manage You can find door UUIDs by listing all access-controlled doors in your organization using the `POST /api/component/findAccessControlledDoors` endpoint in the API Reference. ## Generate a QR Access Code Use the `generateQRAccessCode` endpoint to create a time-bound QR code for door access. ### API Request ```bash cURL theme={null} curl --location 'https://api2.rhombussystems.com/api/accesscontrol/qr/generateQRAccessCode' \ --header 'Accept: application/json' \ --header 'x-auth-scheme: api-token' \ --header 'x-auth-apikey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "accessControlledDoorUuid": "door-uuid-here", "validDurationSec": 86400 }' ``` ```python Python theme={null} import requests import json url = "https://api2.rhombussystems.com/api/accesscontrol/qr/generateQRAccessCode" headers = { "Accept": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "accessControlledDoorUuid": "door-uuid-here", "validDurationSec": 86400 # 24 hours } response = requests.post(url, headers=headers, json=payload) qr_code_data = response.json() print(qr_code_data) ``` ```javascript JavaScript theme={null} const axios = require('axios'); const url = 'https://api2.rhombussystems.com/api/accesscontrol/qr/generateQRAccessCode'; const headers = { 'Accept': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY', 'Content-Type': 'application/json' }; const payload = { accessControlledDoorUuid: 'door-uuid-here', validDurationSec: 86400 // 24 hours }; axios.post(url, payload, { headers }) .then(response => { console.log(response.data); }) .catch(error => { console.error('Error:', error); }); ``` ### Request Parameters The unique identifier of the door you want to authorize access to. This UUID can be obtained from the `findAccessControlledDoors` endpoint. Time in seconds the QR code will remain valid. Common values: * `3600` - 1 hour * `28800` - 8 hours (work day) * `86400` - 24 hours * `604800` - 7 days Optional label identifying who the QR code was issued to (for example, the visitor's name). Rhombus records this value on the resulting access audit events so unlocks can be attributed to the correct person. ### Response The API returns a JSON payload containing the QR code data as a base64-encoded string: ```json theme={null} { "qrCode": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEA...", "accessTokenUuid": "59OcXLkFShWyHK3O2Fh2Ag" } ``` The QR code as a PNG image, returned as a base64-encoded byte array. Decode this to display or distribute the QR code. The UUID of the generated access token backing this QR code. Use it to track or reference the issued credential. ## Implementation Examples ### Convert QR Code to Image After receiving the base64 QR code from the API, you'll need to convert it to a displayable image format. ```python Python theme={null} import base64 from PIL import Image from io import BytesIO # Assuming you have the qr_code_data from the API response qr_code_base64 = qr_code_data['qrCode'] # Decode base64 to image image_data = base64.b64decode(qr_code_base64) image = Image.open(BytesIO(image_data)) # Save to file image.save('access_qr_code.png') # Or display directly image.show() ``` ```javascript Node.js theme={null} const fs = require('fs'); // Assuming you have the response from the API const qrCodeBase64 = response.data.qrCode; // Convert base64 to buffer and save const imageBuffer = Buffer.from(qrCodeBase64, 'base64'); fs.writeFileSync('access_qr_code.png', imageBuffer); console.log('QR code saved to access_qr_code.png'); ``` ### Email QR Code to Visitor Here's a complete example of generating a QR code and emailing it to a visitor: ```python Python theme={null} import requests import base64 from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage import smtplib # Generate QR code url = "https://api2.rhombussystems.com/api/accesscontrol/qr/generateQRAccessCode" headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "accessControlledDoorUuid": "door-uuid-here", "validDurationSec": 28800 # 8 hours } response = requests.post(url, headers=headers, json=payload) qr_data = response.json() # The response does not return an expiry timestamp — derive it from validDurationSec from datetime import datetime, timedelta, timezone valid_until = (datetime.now(timezone.utc) + timedelta(seconds=payload['validDurationSec'])).strftime('%Y-%m-%d %H:%M UTC') # Decode QR code image qr_image = base64.b64decode(qr_data['qrCode']) # Create email msg = MIMEMultipart('related') msg['Subject'] = 'Your Temporary Access QR Code' msg['From'] = 'access@yourcompany.com' msg['To'] = 'visitor@example.com' # Email body html = f"""

Welcome to Our Office

Please use the QR code below to access the building.

This code is valid until {valid_until}

Simply hold your phone up to the camera at the entrance.

""" msg_html = MIMEText(html, 'html') msg.attach(msg_html) # Attach QR code image img = MIMEImage(qr_image) img.add_header('Content-ID', '') msg.attach(img) # Send email smtp = smtplib.SMTP('smtp.gmail.com', 587) smtp.starttls() smtp.login('your-email@gmail.com', 'your-password') smtp.send_message(msg) smtp.quit() print("QR code email sent successfully") ``` ### Integration with Event Management System Generate QR codes for event attendees: ```python Python theme={null} import requests import pandas as pd def generate_event_access_codes(attendees_csv, door_uuid, event_duration_hours): """ Generate QR codes for all event attendees Args: attendees_csv: Path to CSV with attendee information door_uuid: UUID of the door for event access event_duration_hours: How long access should be valid """ # Read attendee list attendees = pd.read_csv(attendees_csv) # API configuration url = "https://api2.rhombussystems.com/api/accesscontrol/qr/generateQRAccessCode" headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } results = [] for _, attendee in attendees.iterrows(): # Generate QR code for each attendee payload = { "accessControlledDoorUuid": door_uuid, "validDurationSec": event_duration_hours * 3600 } response = requests.post(url, headers=headers, json=payload) qr_data = response.json() results.append({ "name": attendee['name'], "email": attendee['email'], "qr_code": qr_data['qrCode'], "access_token_uuid": qr_data['accessTokenUuid'] }) print(f"Generated QR code for {attendee['name']}") # Save results results_df = pd.DataFrame(results) results_df.to_csv('event_qr_codes.csv', index=False) return results_df # Usage attendees = generate_event_access_codes( 'attendees.csv', 'door-uuid-here', event_duration_hours=12 ) ``` ## Security Considerations Always protect your QR codes and implement appropriate security measures: ### Time-Limited Access * Set appropriate `validDurationSec` values based on your use case * Shorter durations (1-8 hours) for visitor access * Longer durations (1-7 days) for contractor or temporary employee access * Never set unlimited duration codes ### QR Code Distribution Use secure email systems and verify recipient addresses before sending QR codes. Consider using encrypted email for sensitive access. Verify phone numbers and use secure SMS services. Be aware that SMS may not be encrypted end-to-end. Integrate QR code generation into your mobile app with proper authentication and user verification. For printed codes, ensure physical security and proper disposal after expiration. Consider adding watermarks or other anti-copying measures. ### Access Monitoring * Review access logs regularly using the Rhombus Console * Set up alerts for unusual access patterns * Monitor failed access attempts * Maintain audit trails of QR code generation and usage ## Best Practices Follow these best practices for a secure and efficient QR code access system: 1. **Validate Door UUIDs**: Always verify door UUIDs before generating QR codes to ensure codes grant access to the correct doors. 2. **Implement Rate Limiting**: If exposing QR code generation through your own application, implement rate limiting to prevent abuse. 3. **Log Generation Events**: Keep records of who generated QR codes, for which doors, and with what validity periods. 4. **User-Friendly Expiration Times**: When displaying QR codes, show the expiration time in the user's local timezone. 5. **Test Before Distribution**: Generate and test QR codes before sending to users to ensure they work correctly. 6. **Provide Instructions**: Include clear instructions with QR codes on where to present them and what to expect. 7. **Error Handling**: Implement proper error handling for API failures and invalid responses. ## Benefits QR code access provides touchless entry, works on any smartphone, and integrates with existing Rhombus access control hardware. ## Troubleshooting **Common causes:** * QR code has expired (it is valid for `validDurationSec` from the time it was generated) * Incorrect door UUID was used when generating the code * Camera is not properly configured for access control * QR code image is damaged or unclear **Solutions:** * Generate a new QR code with a valid duration * Verify the door UUID using the `findAccessControlledDoors` endpoint * Check camera configuration in Rhombus Console * Ensure QR code is displayed clearly and at appropriate size **Common causes:** * Invalid API key or authentication headers * Incorrect door UUID * Door not configured for QR code access * Insufficient permissions **Solutions:** * Verify your API key in the Rhombus Console * Check that `x-auth-scheme` header is set to `api-token` * Confirm the door UUID exists and is configured for access control * Contact Rhombus support if the feature is not enabled for your account **Common causes:** * Poor lighting conditions * QR code too small or too large * Camera angle is incorrect * QR code displayed on a reflective surface **Solutions:** * Ensure adequate lighting at the entry point * Display QR code at 3-5 inches across * Position the QR code perpendicular to the camera * Avoid displaying on glossy screens—use matte screen protectors or print on paper ## Next Steps Set up webhooks to receive real-time access control events Join the Rhombus Developer Community for support and updates Manage your access control devices and settings This feature is actively being developed. Stay tuned to the [Rhombus Developer Community](https://rhombus.community) for updates on new functionality and improvements. # React SDK Source: https://api-docs.rhombus.community/implementations/react-sdk Embed Rhombus camera streams in React applications with the official SDK, featuring DASH buffered playback and real-time H.264 WebSocket players via WebCodecs. The official [`@rhombussystems/react`](https://www.npmjs.com/package/@rhombussystems/react) package provides drop-in React components for streaming Rhombus cameras. Two player modes are available: * **`RhombusBufferedPlayer`** — MPEG-DASH live streaming via Dash.js (buffered, reliable) * **`RhombusRealtimePlayer`** — Low-latency H.264 over WebSocket via WebCodecs (near real-time) **Requires React 18+** and a backend endpoint that generates federated session tokens. Your Rhombus API key must never be exposed to the browser. ## Install ```bash npm theme={null} npm install @rhombussystems/react ``` ```bash yarn theme={null} yarn add @rhombussystems/react ``` ```bash pnpm theme={null} pnpm add @rhombussystems/react ``` **Peer dependencies:** `react` and `react-dom` >= 18. The `dashjs` library is included automatically for DASH playback. ## Quick Start ```tsx theme={null} import { RhombusBufferedPlayer } from "@rhombussystems/react"; export function CameraView() { return ; } ``` This renders a DASH-based buffered video player. The component automatically: 1. Requests a federated session token from your backend (`POST /api/federated-token`) 2. Fetches media URIs from the Rhombus API 3. Initializes Dash.js playback Your backend **must** implement a `POST /api/federated-token` endpoint (or configure a custom path via the `paths.federatedToken` prop). See [Backend Setup](#backend-setup) below. ## Buffered Player (DASH) The `RhombusBufferedPlayer` streams MPEG-DASH live video with configurable quality levels. ```tsx theme={null} import { RhombusBufferedPlayer } from "@rhombussystems/react"; export function BufferedCamera() { return ( ); } ``` ### Stream Quality Control server-side downscaling with the `bufferedStreamQuality` prop: | Value | Description | | ---------- | -------------------------------- | | `"HIGH"` | Full resolution (default) | | `"MEDIUM"` | Medium downscale | | `"LOW"` | Low resolution, lowest bandwidth | Changing quality does not re-fetch the manifest or token — the Dash.js `RequestModifier` applies the change on the next segment request. ```tsx theme={null} import { useState } from "react"; import { RhombusBufferedPlayer, type RhombusBufferedStreamQuality, } from "@rhombussystems/react"; export function CameraWithQuality() { const [quality, setQuality] = useState("HIGH"); return ( <> ); } ``` ## Realtime Player (WebSocket) The `RhombusRealtimePlayer` decodes H.264 frames over WebSocket using the browser's WebCodecs API and renders to a ``. This provides lower latency than DASH. ```tsx theme={null} import { RhombusRealtimePlayer } from "@rhombussystems/react"; export function RealtimeCamera() { return ( ); } ``` ### Connection Modes | Mode | Description | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"wan"` | Uses the WAN H.264 WebSocket URI with federated token auth appended to the URL | | `"lan"` | Uses the LAN H.264 WebSocket URI. Federated token auth is appended to the URL the same way as WAN (`x-auth-scheme=federated-token` and `x-auth-ft` query parameters) | ### Stream Quality | Value | Description | | ------ | ------------------------------------------------------------------------------ | | `"HD"` | Full resolution (default) | | `"SD"` | Lower resolution via `/wsl` path — changing this prop reconnects the WebSocket | **Browser compatibility:** WebCodecs with H.264 decoding is supported in Chrome, Edge, and Safari 16.4+. Firefox support is limited. ### LAN Mode Considerations In LAN mode, the SDK appends the federated token to the WebSocket URL as query parameters (`x-auth-scheme=federated-token` and `x-auth-ft`), the same way it does for WAN. This works from any origin, including `localhost`. The practical requirement for LAN mode is network reachability: the browser must be able to reach the camera/NVR host directly (routing, firewall, and HTTPS-vs-HTTP mixed-content rules all apply). Your Rhombus deployment must also accept the federated-token query parameters on the LAN endpoint. Earlier SDK versions (pre-1.0) authenticated LAN mode with an `RFT` cookie and an `applyLanAuthCookie` prop. That mechanism was removed in v1.0 — LAN now uses URL query parameters like WAN. If you are following an older guide, update your integration accordingly. ## Backend Setup The SDK requires a server-side endpoint to generate federated session tokens. Your Rhombus API key stays on the server — it is never sent to the browser. ### Token Endpoint Your backend must expose a POST endpoint (default path: `/api/federated-token`): ```javascript Express.js theme={null} const express = require("express"); const app = express(); app.use(express.json()); app.post("/api/federated-token", async (req, res) => { const response = await fetch( "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken", { method: "POST", headers: { "Content-Type": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": process.env.RHOMBUS_API_KEY, }, body: JSON.stringify({ durationSec: req.body.durationSec || 3600, domain: req.headers.origin, }), } ); const data = await response.json(); res.json(data); }); ``` ```python FastAPI theme={null} from fastapi import FastAPI, Request import httpx, os app = FastAPI() @app.post("/api/federated-token") async def federated_token(request: Request): body = await request.json() async with httpx.AsyncClient() as client: response = await client.post( "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken", headers={ "Content-Type": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": os.environ["RHOMBUS_API_KEY"], }, json={ "durationSec": body.get("durationSec", 3600), "domain": request.headers.get("origin", ""), }, ) return response.json() ``` ```javascript Next.js API Route theme={null} // app/api/federated-token/route.ts import { NextResponse } from "next/server"; export async function POST(request: Request) { const body = await request.json(); const response = await fetch( "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken", { method: "POST", headers: { "Content-Type": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": process.env.RHOMBUS_API_KEY!, }, body: JSON.stringify({ durationSec: body.durationSec || 3600, domain: request.headers.get("origin") || "", }), } ); const data = await response.json(); return NextResponse.json(data); } ``` The `domain` parameter in the token request must match your app's origin. Without it, the browser will get CORS errors when fetching media URIs from `api2.rhombussystems.com`. ### Override Mode (Proxy All Requests) If you prefer to keep all Rhombus traffic server-side (the browser never talks to Rhombus directly), use `apiOverrideBaseUrl`: ```tsx theme={null} ``` In this mode, your backend must also expose a `POST /api/media-uris` endpoint that proxies to Rhombus's `POST /camera/getMediaUris`. ## Props Reference ### Shared Props | Prop | Type | Default | Description | | ----------------------- | ------------------------ | ------------------------------------------- | ------------------------------------------------------- | | `cameraUuid` | `string` | required | Camera UUID to stream | | `apiOverrideBaseUrl` | `string` | — | Route both token and media requests through your server | | `rhombusApiBaseUrl` | `string` | `https://api2.rhombussystems.com/api` | Rhombus API base URL (when not using override) | | `paths.federatedToken` | `string` | `/api/federated-token` | Path to your token endpoint | | `paths.mediaUris` | `string` | `/camera/getMediaUris` or `/api/media-uris` | Path for media URI resolution | | `federatedSessionToken` | `string` | — | Skip token fetch; use this token directly | | `headers` | `object` | — | Extra headers merged into the token request | | `getRequestHeaders` | `() => object` | — | Dynamic headers for each request | | `onError` | `(error: Error) => void` | — | Error callback | ### RhombusBufferedPlayer Props | Prop | Type | Default | Description | | ---------------------------- | --------------------------------- | -------- | ---------------------------------------- | | `bufferedStreamQuality` | `"HIGH"` \| `"MEDIUM"` \| `"LOW"` | `"HIGH"` | Server-side downscale level | | `applyBufferedStreamQuality` | `boolean` | `true` | Set `false` to disable quality parameter | ### RhombusRealtimePlayer Props | Prop | Type | Default | Description | | ----------------------- | ------------------ | -------- | -------------------------------------------------------------------- | | `connectionMode` | `"wan"` \| `"lan"` | required | WAN or LAN media URIs; both append the token as URL query parameters | | `realtimeStreamQuality` | `"HD"` \| `"SD"` | `"HD"` | Stream resolution; changing reconnects the WebSocket | ## Troubleshooting Your backend does not have a token endpoint at the expected path. Either implement `POST /api/federated-token` or set the `paths.federatedToken` prop to match your route. The federated token was generated without a `domain` matching your app's origin. Pass your app's origin as the `domain` parameter when calling `generateFederatedSessionToken` on your backend. Check browser compatibility — WebCodecs H.264 requires Chrome, Edge, or Safari 16.4+. Check the browser console for `[RhombusRealtimePlayer]` messages. LAN mode appends the federated token to the WebSocket URL as query parameters (the same as WAN), so it works from any origin including `localhost`. If LAN mode fails, confirm the browser can reach the camera/NVR host directly (routing, firewall, and HTTPS-vs-HTTP mixed-content rules), and that your Rhombus deployment accepts federated-token query parameters on the LAN endpoint. If the LAN host is unreachable from the browser, use `connectionMode="wan"` or proxy through your backend. ## Resources Package details and version history Source code, examples, and issues Low-level streaming implementation without the SDK Custom Dash.js player implementation # Reports & Analytics Source: https://api-docs.rhombus.community/implementations/reports-analytics Pull count and time-series analytics from the Rhombus API — people and vehicle counts, occupancy trends, line-crossing (threshold) counts, and CSV export. ## Overview The Rhombus Report Webservice turns detections from your cameras into aggregated, time-bucketed analytics. Instead of iterating over individual events, you request a count report for a time range and interval, and Rhombus returns one data point per bucket with a per-type count. Through the API, you can: * **Build count time-series** for people, vehicles, motion, faces, and other detection types across an org, location, or single device * **Track people counts and occupancy** with the latest readings and bucketed occupancy trends * **Measure line-crossing (threshold) counts** for humans or vehicles that cross a configured line * **Export report data as CSV** for spreadsheets, BI tools, and compliance reporting A few conventions apply across this API: * **Timestamps are epoch milliseconds** on the V2, occupancy, and threshold endpoints (`startTimeMs` / `endTimeMs`). The legacy v1 `getCountReport` uses `"YYYY-MM-DD"` **date strings** instead. * **Reports are described by three enums** — a report type (what to count), an interval (bucket size), and a scope (org, location, or device). See the [reference tables](#enum-reference) below. * **There is no "list report types" endpoint.** The available types are fixed and documented in the [`ReportType` table](#reporttype). ## Prerequisites Before you begin, make sure you have: * A **Rhombus API key** with report permissions (generated in the Rhombus Console under Settings > API) * At least one **camera** deployed and reporting the analytics you want to query (for example, a camera running people counting or LPR) * The **UUID** of the org, location, or device you want to scope the report to * For **CSV export** and audit/diagnostic feeds: the **`REPORT_ADMINISTRATION`** permission on the API key All requests are `POST`, send a JSON body, and use the base URL `https://api2.rhombussystems.com` with these headers: ``` x-auth-scheme: api-token x-auth-apikey: YOUR_API_KEY Content-Type: application/json ``` ## Build a count time-series `getCountReportV2` is the primary reporting endpoint. Supply a time range in epoch milliseconds, an interval, a scope, and one or more report types. Rhombus returns one data point per interval bucket, each carrying an `eventCountMap` keyed by report type. This example pulls hourly **people** and **vehicle** counts for a single location over the last 24 hours. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/report/getCountReportV2", headers=headers, json={ "scope": "LOCATION", "uuid": "YOUR_LOCATION_UUID", "types": ["PEOPLE", "VEHICLES"], "interval": "HOURLY", "startTimeMs": one_day_ago_ms, "endTimeMs": now_ms, "timeZone": "America/Los_Angeles" } ) data = response.json() for point in data.get("timeSeriesDataPoints", []): counts = point.get("eventCountMap", {}) print(f"{point.get('dateLocal')}: " f"people={counts.get('PEOPLE', 0)}, " f"vehicles={counts.get('VEHICLES', 0)}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneDayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/report/getCountReportV2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ scope: 'LOCATION', uuid: 'YOUR_LOCATION_UUID', types: ['PEOPLE', 'VEHICLES'], interval: 'HOURLY', startTimeMs: oneDayAgo, endTimeMs: now, timeZone: 'America/Los_Angeles' }) }); const data = await response.json(); for (const point of data.timeSeriesDataPoints || []) { const counts = point.eventCountMap || {}; console.log(`${point.dateLocal}: people=${counts.PEOPLE || 0}, vehicles=${counts.VEHICLES || 0}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getCountReportV2 \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "scope": "LOCATION", "uuid": "YOUR_LOCATION_UUID", "types": ["PEOPLE", "VEHICLES"], "interval": "HOURLY", "startTimeMs": 1712620800000, "endTimeMs": 1712707200000, "timeZone": "America/Los_Angeles" }' ``` ### Request fields Aggregation level. One of `ORG`, `LOCATION`, `DEVICE`, or `REGION`. See the [`ReportScope` table](#reportscope). The location or device UUID that matches your `scope`. Omit for `scope: "ORG"` to report across the whole organization. One or more report types to count. See the [`ReportType` table](#reporttype). Bucket size for each data point. One of `MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`. See the [`ReportInterval` table](#reportinterval). Start of the range, in epoch milliseconds. End of the range, in epoch milliseconds. Optional IANA time zone (for example, `America/New_York`). Controls where interval bucket boundaries fall and populates `dateLocal`. Defaults to UTC when omitted. The V2 endpoint accepts legacy `startDate` / `endDate` string fields, but these are deprecated. Use the epoch-millisecond `startTimeMs` / `endTimeMs` fields instead. ### Response fields One entry per interval bucket. Bucket start expressed in UTC. Bucket start expressed in the requested `timeZone`. Map of report type to count for this bucket, for example `{"PEOPLE": 42, "VEHICLES": 7}`. Map of report type to the list of device UUIDs that contributed counts in this bucket. Need a single total instead of a bucketed series? Call `/api/report/getSummaryCountReport` with the same `startTimeMs`, `endTimeMs`, `interval`, `scope`, and a single `type`. ### Legacy date-string reports The v1 `/api/report/getCountReport` endpoint predates epoch-millisecond timestamps. It takes `startDate` and `endDate` as `"YYYY-MM-DD"` strings and a single `type` rather than a `types` array. Prefer `getCountReportV2` for new integrations; use v1 only if you already depend on its date-string behavior. ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getCountReport \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "scope": "LOCATION", "uuid": "YOUR_LOCATION_UUID", "type": "PEOPLE", "interval": "DAILY", "startDate": "2024-01-01", "endDate": "2024-01-31" }' ``` ## Build a people-count and occupancy dashboard For a live dashboard, combine two endpoints: `getMostRecentPeopleCountEvents` for the latest readings from a camera, and `getOccupancyCountsV2` for a bucketed occupancy trend over time. ### Latest people-count readings ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } response = requests.post( "https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents", headers=headers, json={ "deviceUuid": "YOUR_CAMERA_UUID", "numMostRecent": 10 } ) data = response.json() for event in data.get("events", []): print(f"[{event.get('eventTimestamp')}] people={event.get('peopleCount')}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ deviceUuid: 'YOUR_CAMERA_UUID', numMostRecent: 10 }) }); const data = await response.json(); for (const event of data.events || []) { console.log(`[${event.eventTimestamp}] people=${event.peopleCount}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "deviceUuid": "YOUR_CAMERA_UUID", "numMostRecent": 10 }' ``` Each entry in `events` is a people-count reading: Unique identifier for the reading. The camera that produced the reading. Location the device belongs to. Organization identifier. Reading time in epoch milliseconds. Number of people counted at this moment. S3 key for the associated frame (a key, not a URL). Detection bounding boxes for the counted people. Labels applied to the device. Labels applied to the location. Sub-location hierarchy key, when configured. ### Occupancy trend over time `getOccupancyCountsV2` returns bucketed occupancy for a device across a time range, using the same interval model as count reports. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/report/getOccupancyCountsV2", headers=headers, json={ "deviceUuid": "YOUR_CAMERA_UUID", "startTimeMs": one_day_ago_ms, "endTimeMs": now_ms, "interval": "HOURLY" } ) data = response.json() for point in data.get("timeSeriesDataPoints", []): print(f"{point.get('dateLocal')}: {point.get('eventCountMap')}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneDayAgo = now - (24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/report/getOccupancyCountsV2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ deviceUuid: 'YOUR_CAMERA_UUID', startTimeMs: oneDayAgo, endTimeMs: now, interval: 'HOURLY' }) }); const data = await response.json(); for (const point of data.timeSeriesDataPoints || []) { console.log(`${point.dateLocal}: ${JSON.stringify(point.eventCountMap)}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getOccupancyCountsV2 \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "deviceUuid": "YOUR_CAMERA_UUID", "startTimeMs": 1712620800000, "endTimeMs": 1712707200000, "interval": "HOURLY" }' ``` The response `timeSeriesDataPoints` carry the same `dateUtc`, `dateLocal`, `eventCountMap`, and `reportingDevicesMap` fields as count reports, plus a `timestampMs` (bucket start in epoch milliseconds) and an `approximateTimestampMsMap`. This occupancy report is derived from camera analytics via the Report Webservice. It is distinct from the Occupancy Webservice (`/api/occupancy/*`), which reads physical BLE and motion sensors — a separate product. ## Count line crossings (threshold counts) When a camera has a crossing line configured, Rhombus counts objects that cross it. `getThresholdCrossingCounts` returns those counts bucketed over time for one or more devices, filtered to a single object class. The `dailyResetTimeMinute` field sets the minute of the day (0–1439, in the device's local time) at which the running daily count resets — useful for aligning counts to a business day rather than midnight. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) one_week_ago_ms = now_ms - (7 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/report/getThresholdCrossingCounts", headers=headers, json={ "devices": ["YOUR_CAMERA_UUID"], "startTimeMs": one_week_ago_ms, "endTimeMs": now_ms, "crossingObject": "HUMAN", "dailyResetTimeMinute": 0 } ) data = response.json() for entry in data.get("counts", []): print(f"[{entry.get('timestampMs')}] crossings={entry.get('count')}") ``` ```javascript JavaScript theme={null} const now = Date.now(); const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/report/getThresholdCrossingCounts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ devices: ['YOUR_CAMERA_UUID'], startTimeMs: oneWeekAgo, endTimeMs: now, crossingObject: 'HUMAN', dailyResetTimeMinute: 0 }) }); const data = await response.json(); for (const entry of data.counts || []) { console.log(`[${entry.timestampMs}] crossings=${entry.count}`); } ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/report/getThresholdCrossingCounts \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "devices": ["YOUR_CAMERA_UUID"], "startTimeMs": 1712016000000, "endTimeMs": 1712620800000, "crossingObject": "HUMAN", "dailyResetTimeMinute": 0 }' ``` ### Request fields One or more camera UUIDs to include in the counts. Start of the range, in epoch milliseconds. End of the range, in epoch milliseconds. The object class to count. Common values are `HUMAN` and `VEHICLE`. See the [`CrossingObject` table](#crossingobject). Minute of the day (0–1439) at which the daily count resets, in the device's local time. ### Response fields Number of crossings in this bucket. Bucket start in epoch milliseconds. ## Export report data as CSV The Export Webservice streams report data as **CSV** (`text/csv`) rather than JSON. There is **no PDF export** — CSV is the only export format. Export endpoints (and the audit/diagnostic feeds) require the **`REPORT_ADMINISTRATION`** permission on your API key. Without it, these calls are rejected. `/api/export/countReports` mirrors the count report parameters but returns a CSV stream. Note that export takes a **single** `type` field, not a `types` array, and both `startTimeMs` and `endTimeMs` are required. ```python Python theme={null} import requests import time headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" } now_ms = int(time.time() * 1000) thirty_days_ago_ms = now_ms - (30 * 24 * 60 * 60 * 1000) response = requests.post( "https://api2.rhombussystems.com/api/export/countReports", headers=headers, json={ "scope": "LOCATION", "uuidList": ["YOUR_LOCATION_UUID"], "type": "PEOPLE", "interval": "DAILY", "startTimeMs": thirty_days_ago_ms, "endTimeMs": now_ms, "timeZone": "America/Los_Angeles" } ) # Response is CSV text, not JSON with open("people_count_report.csv", "w") as f: f.write(response.text) print("Exported report to people_count_report.csv") ``` ```javascript JavaScript theme={null} const now = Date.now(); const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000); const response = await fetch('https://api2.rhombussystems.com/api/export/countReports', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-scheme': 'api-token', 'x-auth-apikey': 'YOUR_API_KEY' }, body: JSON.stringify({ scope: 'LOCATION', uuidList: ['YOUR_LOCATION_UUID'], type: 'PEOPLE', interval: 'DAILY', startTimeMs: thirtyDaysAgo, endTimeMs: now, timeZone: 'America/Los_Angeles' }) }); // Response is CSV text, not JSON const csv = await response.text(); console.log(csv); ``` ```bash cURL theme={null} curl -X POST https://api2.rhombussystems.com/api/export/countReports \ -H "Content-Type: application/json" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -d '{ "scope": "LOCATION", "uuidList": ["YOUR_LOCATION_UUID"], "type": "PEOPLE", "interval": "DAILY", "startTimeMs": 1710028800000, "endTimeMs": 1712620800000, "timeZone": "America/Los_Angeles" }' \ -o people_count_report.csv ``` Because the response body is CSV, read it as text (`response.text` / `.text()`) or write it straight to a file with the `-o` flag in cURL. Do not attempt to parse it as JSON. To export raw people-count readings instead of aggregated reports, use `/api/export/peopleCountEvents` with a `startInterval` and `endInterval` (epoch milliseconds). It returns CSV the same way. ## Enum reference Reports are described by a small set of fixed enums. There is no endpoint that lists these at runtime — the values below are the complete set. ### ReportType Values accepted by the `types` array (V2) and single `type` field (v1 and export). | Value | Counts | | ----------------- | ----------------------------------- | | `CROWD` | Crowd-density detections | | `PEOPLE` | People detections / people counting | | `FACES` | Face detections | | `MOTION` | Motion events | | `BANDWIDTH` | Device bandwidth usage | | `VEHICLES` | Vehicle detections | | `LICENSEPLATES` | License-plate reads | | `ALERTS` | Alert / policy events | | `AM_VERIFICATION` | Alarm-monitoring verifications | | `DWELL` | Dwell-time detections | ### ReportInterval Bucket size for each data point. | Value | Bucket | | --------------- | -------------- | | `MINUTELY` | Per minute | | `QUARTERHOURLY` | Per 15 minutes | | `HOURLY` | Per hour | | `DAILY` | Per day | | `WEEKLY` | Per week | | `MONTHLY` | Per month | ### ReportScope Aggregation level for a count report. Provide a matching `uuid` for `LOCATION`, `DEVICE`, or `REGION`; omit it for `ORG`. | Value | Aggregates over | | ---------- | ----------------------- | | `ORG` | The entire organization | | `LOCATION` | A single location | | `DEVICE` | A single device | | `REGION` | A configured region | ### CrossingObject Object class for `getThresholdCrossingCounts`. | Value | Object | | ------------ | ------------------------ | | `HUMAN` | People | | `VEHICLE` | Vehicles | | `FACE` | Faces | | `LPR` | License plates | | `POSE` | Pose detections | | `CLIP_EMBED` | Visual-embedding matches | | `UNKNOWN` | Unclassified | ## Use cases * **Foot-traffic dashboards** — chart hourly or daily `PEOPLE` counts per location with `getCountReportV2` to compare stores or track trends over time. * **Occupancy monitoring** — combine `getMostRecentPeopleCountEvents` (live reading) with `getOccupancyCountsV2` (trend) to show current and historical occupancy on one screen. * **Entrance and lane counting** — use `getThresholdCrossingCounts` with `crossingObject: "HUMAN"` or `"VEHICLE"` to count people through a doorway or cars through a lane. * **Scheduled reporting** — run `/api/export/countReports` on a cron and drop the CSV into a BI tool or data warehouse for weekly and monthly reporting. ## Advanced: AI Scene Query analytics Rhombus also offers **Scene Query**, an AI feature that counts custom, natural-language-defined events (for example, "forklifts without a spotter"). Scene Query is configured through the `/api/scenequery/*` endpoints and reported via `/api/report/getCustomEventsReport`. It is a distinct, licensed capability rather than part of the core reporting workflow above — treat it as an optional extension when standard report types don't capture what you need to measure. ## Troubleshooting Confirm that the device actually produces the analytic you requested — a camera without people counting enabled returns no `PEOPLE` data. Also verify your time range is in **epoch milliseconds** (not seconds) and that the `scope`/`uuid` pair is correct. Set the `timeZone` field to the relevant IANA zone (for example, `America/Chicago`). Without it, bucket boundaries and `dateLocal` fall on UTC. CSV export requires the `REPORT_ADMINISTRATION` permission. Check the API key's permissions in the Rhombus Console, and remember the export body uses a single `type` field rather than the `types` array used by `getCountReportV2`. The export endpoints return `text/csv`, not JSON. Read the response as text or write it to a file rather than calling `.json()`. ## Next Steps Enroll people, query face events, and dig deeper into people-counting analytics Query license-plate reads and vehicle detections that feed the vehicle report types Explore complete request and response schemas for every report and export endpoint # Retrieving Audio from A100 Audio Gateway Source: https://api-docs.rhombus.community/implementations/retrieving-audio Download recorded audio from a Rhombus A100 Audio Gateway using the API — fetch DASH MPD manifests, retrieve segments, and play back audio over WAN VOD. In this guide, you will learn how to retrieve **recorded audio** from a Rhombus **A100 Audio Gateway** using the Rhombus API. You will: 1. Call the API to get media URI templates 2. Generate a federated session token for WAN playback 3. Download the DASH MPD and audio segments 4. Optionally convert the resulting Opus/WebM audio using ffmpeg Rhombus exposes media access through **URI templates** rather than returning raw audio bytes directly from a single REST response. ## Prerequisites All API calls in this guide use the following authentication headers: ```http theme={null} Content-Type: application/json x-auth-scheme: api-token x-auth-apikey: YOUR_API_KEY ``` Base URL: `https://api2.rhombussystems.com` **You will need:** * An A100 Audio Gateway UUID (Rhombus DeviceFacetUuid / RUUID style string) * Recorded audio that exists and is available (determined by your device licensing and recording configuration) * A client capable of: * Making HTTP requests * Parsing XML (for the MPD manifest) * Writing binary segment bytes to disk ## Implementation Steps Call the `getMediaUris` endpoint to retrieve the URI templates for your audio gateway. **Endpoint:** `POST /api/audiogateway/getMediaUris` ```bash theme={null} curl --request POST \ --url https://api2.rhombussystems.com/api/audiogateway/getMediaUris \ --header 'Content-Type: application/json' \ --header 'x-auth-scheme: api-token' \ --header 'x-auth-apikey: YOUR_API_KEY' \ --data '{ "gatewayUuid": "AAAAAAAAAAAAAAAAAAAAAA.v0" }' ``` **Relevant response fields:** | Field | Description | | ------------------------ | ----------------------------------------------------------- | | `wanVodMpdUriTemplate` | WAN VOD MPD template (use this for recorded audio over WAN) | | `wanLiveOpusUri` | WAN live Opus stream URI | | `wanLiveMpdUri` | WAN live MPD URI | | `lanVodMpdUrisTemplates` | LAN VOD templates | WAN MPD access requires a short-lived federated session token appended as query parameters. **Endpoint:** `POST /api/org/generateFederatedSessionToken` ```bash theme={null} curl --request POST \ --url https://api2.rhombussystems.com/api/org/generateFederatedSessionToken \ --header 'Content-Type: application/json' \ --header 'x-auth-scheme: api-token' \ --header 'x-auth-apikey: YOUR_API_KEY' \ --data '{ "domain": ".rhombussystems.com", "durationSec": 60 }' ``` **Response:** Returns `federatedSessionToken` Choose a `durationSec` long enough to fetch the MPD and all segments for your requested range. For large pulls (up to 24 hours), increase this value accordingly. Take the `wanVodMpdUriTemplate` from Step 1 and perform string substitution: * Replace `{START_TIME}` with your requested start time (in seconds) * Replace `{DURATION}` with your requested duration (in seconds) **Timestamp units matter!** Rhombus audio endpoints use **seconds since epoch** (`startTimeSec`, `durationSec`). If your timestamps are stored in milliseconds, convert them: ```text theme={null} startTimeSec = startTimeMs / 1000 durationSec = durationMs / 1000 ``` Append the authentication query parameters to your MPD URL: ```text theme={null} ?x-auth-scheme=federated-token&x-auth-ft= ``` This MPD is a DASH manifest that describes how to fetch the init segment and subsequent audio segments. Download the segments in order, appending the same `?x-auth-scheme=federated-token&x-auth-ft=` query parameters you used for the MPD — the segments are served from the same WAN host and use the same authentication: 1. **Init segment** (e.g., `seg_init_audio.hdr`) 2. **Media segments** (e.g., `seg_1.webm`, `seg_2.webm`, ...) Segments are **2 seconds each**. Calculate the number of segments: ```text theme={null} numSegments = durationSec / 2 ``` Write the init header bytes followed by each segment's bytes to produce a valid WebM/Opus audio file (48 kHz, mono). *** ## Complete Python Example This example implements the full workflow described above: ```python theme={null} import requests BASE_URL = "https://api2.rhombussystems.com" API_KEY = "YOUR_API_KEY" HEADERS = { "Content-Type": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": API_KEY, } def generate_federated_token(domain: str, duration_sec: int) -> str: """Generate a short-lived federated session token for WAN access.""" url = f"{BASE_URL}/api/org/generateFederatedSessionToken" resp = requests.post( url, headers=HEADERS, json={"domain": domain, "durationSec": duration_sec} ) resp.raise_for_status() return resp.json()["federatedSessionToken"] def get_audio_gateway_media_uris(gateway_uuid: str) -> dict: """Get media URI templates for an audio gateway.""" url = f"{BASE_URL}/api/audiogateway/getMediaUris" resp = requests.post(url, headers=HEADERS, json={"gatewayUuid": gateway_uuid}) resp.raise_for_status() return resp.json() def build_mpd_uri(template: str, start_time_sec: int, duration_sec: int) -> str: """Substitute placeholders in the MPD URI template.""" return template.replace("{START_TIME}", str(start_time_sec)).replace( "{DURATION}", str(duration_sec) ) def download_wan_vod_audio( gateway_uuid: str, start_time_sec: int, duration_sec: int, out_path: str ): """Download recorded audio from a Rhombus A100 Audio Gateway.""" # Step 1: Get media URIs media = get_audio_gateway_media_uris(gateway_uuid) mpd_template = media["wanVodMpdUriTemplate"] # Step 2: Generate federated token ft = generate_federated_token(domain=".rhombussystems.com", duration_sec=60) # Step 3: Build MPD URL mpd_uri = build_mpd_uri(mpd_template, start_time_sec, duration_sec) # Step 4: Fetch MPD (authenticated via query string) auth_qs = f"?x-auth-scheme=federated-token&x-auth-ft={ft}" mpd_resp = requests.get(mpd_uri + auth_qs) mpd_resp.raise_for_status() # Step 5: Download init segment and media segments. # These are served from the same WAN host as the MPD, so authenticate them # with the federated token query params — not the API key header. init_uri = mpd_uri.replace("file.mpd", "seg_init_audio.hdr") num_segments = duration_sec // 2 with open(out_path, "wb") as f: # Download init segment init_resp = requests.get(init_uri + auth_qs) init_resp.raise_for_status() f.write(init_resp.content) # Download media segments for i in range(1, int(num_segments) + 1): seg_uri = mpd_uri.replace("file.mpd", f"seg_{i}.webm") seg_resp = requests.get(seg_uri + auth_qs) seg_resp.raise_for_status() f.write(seg_resp.content) if __name__ == "__main__": gateway_uuid = "AAAAAAAAAAAAAAAAAAAAAA.v0" start_time_sec = 1700000000 # Example: November 14, 2023 duration_sec = 60 # 1 minute (max: 86400 seconds / 24 hours) download_wan_vod_audio( gateway_uuid=gateway_uuid, start_time_sec=start_time_sec, duration_sec=duration_sec, out_path="rhombus_audio.webm", ) print("Wrote rhombus_audio.webm") ``` ## Converting Audio with ffmpeg The downloaded audio is in **Opus/WebM** format (48 kHz, mono). You can convert it to other formats using ffmpeg: ```bash WAV (16-bit PCM) theme={null} ffmpeg -i rhombus_audio.webm -acodec pcm_s16le rhombus_audio.wav ``` ```bash MP3 theme={null} ffmpeg -i rhombus_audio.webm rhombus_audio.mp3 ``` ## Troubleshooting **Cause:** Template substitution uses wrong time units. **Solution:** If your timestamps are in milliseconds but the template expects seconds, convert them: ```python theme={null} start_time_sec = start_time_ms // 1000 duration_sec = duration_ms // 1000 ``` **Cause:** The code assumes the MPD filename is `file.mpd`. **Solution:** If your template uses a different filename, adjust the string replacement logic: ```python theme={null} # Current logic init_uri = mpd_uri.replace("file.mpd", "seg_init_audio.hdr") # Adjust to match your actual filename init_uri = mpd_uri.replace("your_filename.mpd", "seg_init_audio.hdr") ``` **Cause:** The federated token duration is too short for large audio pulls. **Solution:** Increase `durationSec` when generating the token. For 24-hour pulls, you may need significantly longer durations to fetch all segments. ```python theme={null} ft = generate_federated_token(domain=".rhombussystems.com", duration_sec=300) # 5 minutes ``` ## API Reference The following endpoints are used in this guide. Visit the [API Reference](/api-reference) tab for full request and response schemas. * `POST /api/audiogateway/getMediaUris` — Get Media URIs for Audio Gateway * `POST /api/org/generateFederatedSessionToken` — Generate Federated Session Token # SAML SSO & SCIM Provisioning Source: https://api-docs.rhombus.community/implementations/saml-sso-provisioning Configure SAML single sign-on and SCIM user provisioning for Rhombus via the API — integrate Okta, Azure AD, Google Workspace, OneLogin, and other IdPs. ## Overview Rhombus supports **SAML 2.0** so your employees can sign in to the Rhombus Console with their existing corporate identity (Okta, Azure AD / Microsoft Entra, Google Workspace, OneLogin, and any SAML-compliant IdP), and **SCIM 2.0** so user lifecycle changes in your IdP propagate to Rhombus automatically. This guide covers the **API** surface for configuring both. Use it when you need to: * **Manage Rhombus as infrastructure-as-code** (Terraform, Pulumi, in-house CI scripts) * **Rotate IdP signing certificates** on a scheduled cadence * **Onboard many Rhombus orgs** from a partner or MSP control plane * **Audit or diff** identity configuration across environments * **Stand up a new tenant** end-to-end without clicking through the Console If you only need to configure SSO once for a single organization, the [Rhombus Console](https://console.rhombussystems.com/) has a UI for everything in this guide — but everything the UI can do is exposed via the API. **What this guide isn't.** If you're a third-party developer building an application that authenticates Rhombus users (a "Sign in with Rhombus" flow), you want [Sign in with Rhombus](/oauth-authentication) instead. That's OAuth 2.0 with Rhombus as the Identity Provider — a completely separate surface from the workforce-SSO configuration covered here. ## Architecture at a glance Two independent flows share your IdP but don't otherwise depend on each other: ```mermaid theme={null} flowchart LR subgraph IdP["Your IdP (Okta, Azure AD, Google, etc.)"] Users[Users & Groups] Meta[SAML Metadata XML] end subgraph Rhombus["Rhombus"] Console[Rhombus Console
Sign-in page] API[api2.rhombussystems.com] SCIM[SCIM endpoint
scimEndpointUrl from getScimDisplayInfo] end Users -- "SAML assertion on login" --> Console Meta -. "uploaded via updateSAMLSettingsV2" .-> API Users -- "SCIM create/update/delete" --> SCIM API -- "exposes scimEndpointUrl" --> IdP ``` * **SAML SSO** authenticates users at sign-in time. Your IdP posts a SAML assertion to Rhombus; Rhombus validates it against the IdP metadata XML you uploaded. * **SCIM** propagates user lifecycle events (create, update, deactivate) from your IdP to Rhombus continuously, without requiring users to sign in first. You can run either one alone, but most deployments run both: **SAML for sign-in, SCIM for provisioning**. ## Before you begin Before you start, make sure you have: * A **Rhombus API key** with organization-admin permissions (generated in the [Rhombus Console](https://console.rhombussystems.com/) under **Settings → API Management**) * An **identity provider** that supports SAML 2.0 and (for provisioning) SCIM 2.0 * The **IdP metadata XML** for your SAML application, either as a file or URL * A recovery user account with a password that **bypasses SAML** — see [break-glass access](#break-glass-access) before you enable SSO *** # Part 1: SAML SSO Two endpoints cover the entire SAML configuration lifecycle: | Endpoint | Purpose | | ------------------------------------ | ----------------------------------------------- | | `POST /api/org/getSAMLSettingsV2` | Read current SAML settings for the organization | | `POST /api/org/updateSAMLSettingsV2` | Replace SAML settings | `getSAMLSettings` (v1, no `V2` suffix) and `updateSAMLSettings` (v1) are **deprecated**. Use the V2 endpoints shown in this guide. ## Read current SAML settings Fetch the current configuration so you can diff against what you intend to push, or confirm a previous update landed. ```python Python theme={null} import requests headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", } response = requests.post( "https://api2.rhombussystems.com/api/org/getSAMLSettingsV2", headers=headers, json={}, ) for setting in response.json().get("samlSettings", []): print(f"team={setting.get('teamName')} enabled={setting.get('enabled')} " f"jit={setting.get('justInTimeAccountProvisioningEnabled')}") ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api2.rhombussystems.com/api/org/getSAMLSettingsV2", { method: "POST", headers: { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({}), } ); const { samlSettings = [] } = await response.json(); for (const s of samlSettings) { console.log(`team=${s.teamName} enabled=${s.enabled} jit=${s.justInTimeAccountProvisioningEnabled}`); } ``` ```go Go theme={null} req, _ := http.NewRequest("POST", "https://api2.rhombussystems.com/api/org/getSAMLSettingsV2", strings.NewReader("{}")) req.Header.Set("x-auth-scheme", "api-token") req.Header.Set("x-auth-apikey", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var result struct { SamlSettings []struct { TeamName string `json:"teamName"` Enabled bool `json:"enabled"` JustInTimeAccountProvisioningEnabled bool `json:"justInTimeAccountProvisioningEnabled"` } `json:"samlSettings"` } json.NewDecoder(resp.Body).Decode(&result) ``` The response contains a `samlSettings` array. Most orgs have one entry — a second entry exists only for deployments that span both `rhombus.com` and `rhombussystems.com` domains. ## Configure SAML `POST /api/org/updateSAMLSettingsV2` replaces the organization's SAML configuration. Include the entries you want to exist after the call — fields omitted inside an entry revert to defaults. ### The `OrgSamlSettingsType` fields | Field | Type | Description | | -------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | boolean | Turn SAML sign-in on or off. Set to `false` to disable SSO without losing your metadata. | | `idpMetaDataXml` | string | The raw SAML metadata XML from your IdP (entity descriptor, signing cert, SSO URLs). | | `justInTimeAccountProvisioningEnabled` | boolean | If `true`, a Rhombus user is auto-created the first time someone signs in via SAML. See [JIT vs SCIM](#jit-vs-scim) below. | | `enabledForRhombusKey` | boolean | Also require SAML for the Rhombus Key mobile access app. | | `addUsersOnRoleMismatch` | boolean | If `true`, sign-in succeeds even when the IdP-asserted role doesn't match a Rhombus role; the user is added with a default role. | | `teamName` | string | Display label used in the Console. | | `domain` | enum | `RHOMBUS_COM` or `RHOMBUS_SYSTEMS_COM`. Most customers use `RHOMBUS_COM`. | | `rhombusKeyAppSettings` | object | Per-app toggles for the Rhombus Key mobile app (remote unlock, SAML bypass for mobile, etc.). | ### Upload IdP metadata ```python Python theme={null} import requests # Read your IdP's federation metadata XML with open("idp-metadata.xml", "r") as f: idp_metadata_xml = f.read() headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", } body = { "samlSettings": [ { "enabled": True, "idpMetaDataXml": idp_metadata_xml, "justInTimeAccountProvisioningEnabled": True, "enabledForRhombusKey": True, "addUsersOnRoleMismatch": False, "teamName": "Acme Corp", "domain": "RHOMBUS_COM", } ] } response = requests.post( "https://api2.rhombussystems.com/api/org/updateSAMLSettingsV2", headers=headers, json=body, ) response.raise_for_status() print("SAML configuration updated.") ``` ```javascript JavaScript theme={null} import fs from "node:fs/promises"; const idpMetadataXml = await fs.readFile("idp-metadata.xml", "utf8"); const response = await fetch( "https://api2.rhombussystems.com/api/org/updateSAMLSettingsV2", { method: "POST", headers: { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ samlSettings: [ { enabled: true, idpMetaDataXml: idpMetadataXml, justInTimeAccountProvisioningEnabled: true, enabledForRhombusKey: true, addUsersOnRoleMismatch: false, teamName: "Acme Corp", domain: "RHOMBUS_COM", }, ], }), } ); if (!response.ok) throw new Error(`Update failed: ${await response.text()}`); console.log("SAML configuration updated."); ``` ```go Go theme={null} xmlBytes, _ := os.ReadFile("idp-metadata.xml") body, _ := json.Marshal(map[string]any{ "samlSettings": []map[string]any{ { "enabled": true, "idpMetaDataXml": string(xmlBytes), "justInTimeAccountProvisioningEnabled": true, "enabledForRhombusKey": true, "addUsersOnRoleMismatch": false, "teamName": "Acme Corp", "domain": "RHOMBUS_COM", }, }, }) req, _ := http.NewRequest("POST", "https://api2.rhombussystems.com/api/org/updateSAMLSettingsV2", bytes.NewReader(body)) req.Header.Set("x-auth-scheme", "api-token") req.Header.Set("x-auth-apikey", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil || resp.StatusCode != 200 { // handle error } ``` `updateSAMLSettingsV2` is a **replacement** operation. To change one field (say, `teamName`), read the current settings first, modify the entry you want, and send the full array back. Omitting an entry that previously existed removes it. ## JIT vs SCIM You have two options for getting users into Rhombus: | Option | What it does | When to choose it | | ------------------------------------------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | **JIT** (`justInTimeAccountProvisioningEnabled: true`) | A Rhombus user record is created the first time a person signs in via SAML. | Small teams, low churn, SCIM unavailable on your IdP tier. | | **SCIM** ([Part 2](#part-2-scim-provisioning)) | Your IdP pushes create, update, and deactivate events to Rhombus as they happen. | Larger orgs, compliance-driven deprovisioning requirements, role changes that must propagate immediately. | You can enable both — JIT handles any user who signs in before SCIM syncs them, and SCIM keeps the directory consistent thereafter. Most production deployments run both. ## Rotating IdP metadata IdP signing certificates rotate. Build a scheduled job that pulls fresh metadata from your IdP and pushes it to Rhombus — Rhombus keeps accepting assertions signed with the old cert until you replace the metadata. ```python Python theme={null} import requests def rotate_saml_metadata(api_key: str, new_metadata_xml: str) -> None: """Fetch current SAML settings, swap in new IdP metadata, push back.""" headers = { "x-auth-scheme": "api-token", "x-auth-apikey": api_key, "Content-Type": "application/json", } # 1. Read current settings current = requests.post( "https://api2.rhombussystems.com/api/org/getSAMLSettingsV2", headers=headers, json={}, ).json() settings = current.get("samlSettings") or [] if not settings: raise RuntimeError("No existing SAML settings to rotate.") # 2. Replace the metadata XML on each entry; leave other fields untouched for entry in settings: entry["idpMetaDataXml"] = new_metadata_xml # 3. Push back response = requests.post( "https://api2.rhombussystems.com/api/org/updateSAMLSettingsV2", headers=headers, json={"samlSettings": settings}, ) response.raise_for_status() ``` Schedule the rotation well before your IdP cert actually expires. Verify a real sign-in immediately after rotation, before closing the maintenance window — a malformed XML update can prevent logins until you revert. ## Break-glass access **Before you enable SAML, make sure at least one admin user has a Rhombus-native password and can bypass SAML.** Otherwise a misconfiguration locks everyone out and the only recovery path is Rhombus Support. Every Rhombus user record has a `bypassSaml` boolean. Keep one or two admin accounts with `bypassSaml: true` as a recovery path. These accounts should: * Use strong, unique passwords stored in your secrets manager * Have MFA enabled * Be audited regularly — treat them like root credentials Never disable SAML **and** remove all break-glass accounts in the same change. Test your recovery path at least once per quarter. *** # Part 2: SCIM provisioning SCIM (System for Cross-domain Identity Management) lets your IdP push user lifecycle events directly to Rhombus — no sign-in required. Five endpoints cover SCIM end-to-end: | Endpoint | Purpose | | ---------------------------------------- | --------------------------------------------------------- | | `POST /api/org/getScimDisplayInfo` | Get the SCIM endpoint URLs to hand to your IdP | | `POST /api/org/setupSCIMAccessForOrg` | First-time SCIM setup; returns a bearer token | | `POST /api/org/findSCIMSettingsForOrg` | Read current SCIM configuration | | `POST /api/org/updateSCIMSettingsForOrg` | Change SCIM options (welcome emails, role behavior, etc.) | | `POST /api/org/revokeSCIMAccessForOrg` | Invalidate the current SCIM bearer token | ## Get the SCIM endpoint URLs Your IdP needs two pieces of information to connect: the **endpoint URL** and a **bearer token**. The endpoint URL comes from `getScimDisplayInfo`: ```python Python theme={null} import requests response = requests.post( "https://api2.rhombussystems.com/api/org/getScimDisplayInfo", headers={ "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, json={}, ) info = response.json() print("Standard SCIM endpoint:", info["scimEndpointUrl"]) print("Azure AD SCIM endpoint:", info["azureScimEndpointUrl"]) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api2.rhombussystems.com/api/org/getScimDisplayInfo", { method: "POST", headers: { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({}), } ); const info = await response.json(); console.log("Standard SCIM endpoint:", info.scimEndpointUrl); console.log("Azure AD SCIM endpoint:", info.azureScimEndpointUrl); ``` ```go Go theme={null} req, _ := http.NewRequest("POST", "https://api2.rhombussystems.com/api/org/getScimDisplayInfo", strings.NewReader("{}")) req.Header.Set("x-auth-scheme", "api-token") req.Header.Set("x-auth-apikey", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var info struct { ScimEndpointUrl string `json:"scimEndpointUrl"` AzureScimEndpointUrl string `json:"azureScimEndpointUrl"` } json.NewDecoder(resp.Body).Decode(&info) ``` Most IdPs use `scimEndpointUrl`. Azure AD / Entra requires the Azure-specific variant due to Microsoft-specific schema quirks. ## Initial SCIM setup The first call provisions a bearer token. **This token is shown once and cannot be retrieved later** — capture it immediately into your secrets manager. ```python Python theme={null} import requests response = requests.post( "https://api2.rhombussystems.com/api/org/setupSCIMAccessForOrg", headers={ "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "sendWelcomeEmailToNewUsers": True, "sendWelcomeEmailToNewRhombusKeyUsers": True, "addUsersOnRoleMismatch": False, }, ) data = response.json() if data.get("scimAccessAlreadySetupFailure"): raise RuntimeError("SCIM is already set up — revoke first or use update.") scim_bearer_token = data["token"] # Store scim_bearer_token in your secrets manager immediately. # Paste it into your IdP's SCIM provisioning settings as the bearer token. ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api2.rhombussystems.com/api/org/setupSCIMAccessForOrg", { method: "POST", headers: { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ sendWelcomeEmailToNewUsers: true, sendWelcomeEmailToNewRhombusKeyUsers: true, addUsersOnRoleMismatch: false, }), } ); const data = await response.json(); if (data.scimAccessAlreadySetupFailure) { throw new Error("SCIM is already set up — revoke first or use update."); } const scimBearerToken = data.token; // Store scimBearerToken in your secrets manager immediately. ``` ```go Go theme={null} body, _ := json.Marshal(map[string]any{ "sendWelcomeEmailToNewUsers": true, "sendWelcomeEmailToNewRhombusKeyUsers": true, "addUsersOnRoleMismatch": false, }) req, _ := http.NewRequest("POST", "https://api2.rhombussystems.com/api/org/setupSCIMAccessForOrg", bytes.NewReader(body)) req.Header.Set("x-auth-scheme", "api-token") req.Header.Set("x-auth-apikey", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var data struct { Token string `json:"token"` ScimAccessAlreadySetupFailure bool `json:"scimAccessAlreadySetupFailure"` } json.NewDecoder(resp.Body).Decode(&data) // Store data.Token in your secrets manager immediately. ``` The SCIM bearer token is shown **once**. Losing it means revoking the current access and setting up again — and your IdP loses sync until its token is updated. ### Setup options | Field | Type | Description | | -------------------------------------- | ------- | -------------------------------------------------------------------- | | `sendWelcomeEmailToNewUsers` | boolean | Email new users when SCIM creates their Rhombus account. | | `sendWelcomeEmailToNewRhombusKeyUsers` | boolean | Same, but for users added to the Rhombus Key mobile access app. | | `addUsersOnRoleMismatch` | boolean | Create the user anyway if the IdP-asserted role doesn't map cleanly. | | `rhombusKeyAppSettings` | object | Per-app toggles for Rhombus Key provisioning. | ## Read current SCIM settings `findSCIMSettingsForOrg` returns the full `SCIMSettingsType` record, including the **role format** setting, which matters for IdP compatibility: ```python Python theme={null} response = requests.post( "https://api2.rhombussystems.com/api/org/findSCIMSettingsForOrg", headers=headers, json={}, ) settings = response.json().get("scimSettings", {}) print("rolesFormat:", settings.get("rolesFormat")) # LIST_OF_STRINGS or LIST_OF_MULTI_VALUED_ATTRIBUTES print("welcome emails:", settings.get("sendWelcomeEmailToNewUsers")) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api2.rhombussystems.com/api/org/findSCIMSettingsForOrg", { method: "POST", headers, body: "{}" } ); const { scimSettings = {} } = await response.json(); console.log("rolesFormat:", scimSettings.rolesFormat); ``` ```go Go theme={null} req, _ := http.NewRequest("POST", "https://api2.rhombussystems.com/api/org/findSCIMSettingsForOrg", strings.NewReader("{}")) req.Header.Set("x-auth-scheme", "api-token") req.Header.Set("x-auth-apikey", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") // ... decode response.scimSettings ``` ### Role format compatibility Rhombus accepts SCIM role assertions in two shapes via `rolesFormat`: | Value | Shape | Typical IdP | | --------------------------------- | ---------------------------------------------------- | ---------------------- | | `LIST_OF_STRINGS` | `"roles": ["admin", "viewer"]` | Okta, OneLogin, Google | | `LIST_OF_MULTI_VALUED_ATTRIBUTES` | `"roles": [{"value": "admin"}, {"value": "viewer"}]` | Azure AD / Entra | If users are being created but their roles aren't assigned correctly, verify this setting against what your IdP actually sends. ## Update SCIM settings `updateSCIMSettingsForOrg` changes SCIM behavior without re-issuing the bearer token: ```python Python theme={null} response = requests.post( "https://api2.rhombussystems.com/api/org/updateSCIMSettingsForOrg", headers=headers, json={ "sendWelcomeEmailToNewUsers": False, "sendWelcomeEmailToNewRhombusKeyUsers": True, "addUsersOnRoleMismatch": True, }, ) response.raise_for_status() ``` ## Rotate or revoke the SCIM bearer token To rotate: **revoke, then set up again**. Your IdP must be updated with the new token in the same maintenance window or provisioning will fail. ```python Python theme={null} # 1. Revoke current access requests.post( "https://api2.rhombussystems.com/api/org/revokeSCIMAccessForOrg", headers=headers, json={}, ).raise_for_status() # 2. Issue a new token with the same options as before new = requests.post( "https://api2.rhombussystems.com/api/org/setupSCIMAccessForOrg", headers=headers, json={ "sendWelcomeEmailToNewUsers": True, "sendWelcomeEmailToNewRhombusKeyUsers": True, "addUsersOnRoleMismatch": False, }, ).json() new_token = new["token"] # 3. Push new_token into your IdP's SCIM bearer-token field. ``` ```javascript JavaScript theme={null} // 1. Revoke await fetch("https://api2.rhombussystems.com/api/org/revokeSCIMAccessForOrg", { method: "POST", headers, body: "{}" }); // 2. Re-issue const setupResp = await fetch( "https://api2.rhombussystems.com/api/org/setupSCIMAccessForOrg", { method: "POST", headers, body: JSON.stringify({ sendWelcomeEmailToNewUsers: true, sendWelcomeEmailToNewRhombusKeyUsers: true, addUsersOnRoleMismatch: false, }), } ); const { token: newToken } = await setupResp.json(); // 3. Update IdP with newToken. ``` ```go Go theme={null} revokeReq, _ := http.NewRequest("POST", "https://api2.rhombussystems.com/api/org/revokeSCIMAccessForOrg", strings.NewReader("{}")) // set headers, execute ... // Then setup again to get a new token (see earlier example). ``` Revoking SCIM immediately stops your IdP from syncing users. Existing Rhombus users are unaffected — they remain active and can still sign in via SAML — but any IdP changes (new hires, deprovisioned users) won't propagate until SCIM is restored. *** ## IdP-specific setup notes The API calls above are the same across every IdP. What differs is where to paste the Rhombus values in your IdP's console. These notes capture what customers most often need. IdP consoles change their UI frequently. Treat these as starting points and fall back to the IdP's own documentation if a menu has moved. **SAML.** Create a new **SAML 2.0** application. For the SSO URL and Audience URI, use the values from your organization's Rhombus SAML ACS (visible in the Console under **Settings → SSO / SAML**). Download the Okta IdP metadata XML from the application's **Sign On** tab and pass it to `updateSAMLSettingsV2` as `idpMetaDataXml`. **SCIM.** In the Okta app's **Provisioning** tab, select **SCIM 2.0**. Set the **SCIM connector base URL** to the `scimEndpointUrl` from `getScimDisplayInfo`. Set **Authentication Mode** to **HTTP Header**, with `Authorization: Bearer `. Set `rolesFormat` to `LIST_OF_STRINGS`. **SAML.** Create a new **Enterprise Application** → **Non-gallery**. On **Single sign-on**, choose SAML and import Rhombus's SP metadata. Download the **Federation Metadata XML** from Azure and pass it to `updateSAMLSettingsV2`. **SCIM.** On **Provisioning**, choose **Automatic**. Use the **`azureScimEndpointUrl`** (not the standard endpoint) as the Tenant URL, and paste the bearer token from `setupSCIMAccessForOrg` as the Secret Token. Set `rolesFormat` to **`LIST_OF_MULTI_VALUED_ATTRIBUTES`** — Azure sends role assertions in this shape. **SAML.** From the Google Admin Console, add a custom SAML app targeting Rhombus. Download the Google IdP metadata and pass it to `updateSAMLSettingsV2`. **SCIM.** Google Workspace supports SCIM for some apps via **Automated user provisioning**. Use the `scimEndpointUrl` with bearer token authentication. Set `rolesFormat` to `LIST_OF_STRINGS`. **SAML.** Create a new SAML 2.0 connector; configure the ACS URL and Entity ID from Rhombus's SP metadata. Download the OneLogin IdP metadata XML and pass it to `updateSAMLSettingsV2`. **SCIM.** OneLogin's SCIM v2 provisioning takes the standard `scimEndpointUrl` and a bearer token. Set `rolesFormat` to `LIST_OF_STRINGS`. *** ## Auditing SSO events Every SAML login attempt — success or failure — is written to the Rhombus audit log, as are changes to SAML configuration itself. Pull these events via `POST /api/report/getAuditFeed`. Audit event types relevant to SSO: | Event type | Fires when | | --------------------------- | ------------------------------------------------------- | | `SAML_LOGIN_WEB` | A user successfully signs in via SAML on the Console | | `SAML_LOGIN_FAILURE_WEB` | A SAML sign-in failed on the Console | | `SAML_LOGIN_MOBILE` | A user signed in via SAML on a Rhombus mobile app | | `SAML_LOGIN_FAILURE_MOBILE` | A SAML sign-in failed on a Rhombus mobile app | | `RHOMBUS_KEY_SAML_LOGIN` | A user signed in via SAML on the Rhombus Key mobile app | | `UPDATE_INTEGRATION_SAML` | SAML configuration itself was changed | Stream `UPDATE_INTEGRATION_SAML` events into your SIEM to detect unauthorized changes to SSO configuration, and alert on any `SAML_LOGIN_FAILURE_*` spike to catch broken rotations fast. ## Troubleshooting Most often an issue with the metadata XML itself rather than the API call. * **Verify the XML** is well-formed and contains a current signing certificate — an expired cert in the metadata blocks every assertion. * **Verify the entity ID** in the metadata matches what Rhombus expects (visible in the Console under **Settings → SSO**). * **Verify JIT is enabled** if this is the first sign-in for these users and SCIM hasn't synced them yet — otherwise Rhombus has no user to map the assertion to. * **Check `UPDATE_INTEGRATION_SAML` audit events** to confirm the change landed as intended. This is almost always `rolesFormat` mismatch. Call `findSCIMSettingsForOrg`, verify the value, and cross-reference against the [role format compatibility table](#role-format-compatibility). Azure AD needs `LIST_OF_MULTI_VALUED_ATTRIBUTES`; most others need `LIST_OF_STRINGS`. SCIM is already configured. Either (a) call `revokeSCIMAccessForOrg` and re-run setup to get a fresh token, or (b) call `updateSCIMSettingsForOrg` to modify options without touching the token. The IdP is still using the old bearer token. After `revokeSCIMAccessForOrg` + fresh `setupSCIMAccessForOrg`, the new token **must** be pasted into the IdP's provisioning config. Do both in one maintenance window to avoid a sync gap. Use a break-glass account with `bypassSaml: true` and a Rhombus-native password. If none exists, contact Rhombus Support. This is why [break-glass access](#break-glass-access) is a prerequisite, not a nice-to-have. Set `addUsersOnRoleMismatch: false` to reject users whose IdP role doesn't map, forcing the IdP operator to fix the assertion. Set it to `true` if you prefer to create the user and assign a role in Rhombus after the fact. Pick deliberately — the two behaviors are mutually exclusive. ## Next steps Build third-party apps that authenticate Rhombus users (OAuth 2.0, separate from workforce SSO) Full schema for every endpoint used in this guide (Org tag) Understand request limits for scripted rotations and audits Ask SSO questions and share IdP-specific setup tips # Stream Live and Recorded Video from Cameras Source: https://api-docs.rhombus.community/implementations/streaming-video Embed live and recorded video from Rhombus cameras in your applications using HLS streams, thumbnails, and shared playback URLs delivered through the API. ## Overview Rhombus cameras expose multiple ways to access video content through the public API: * **Live streaming** for real-time monitoring, embedded as a Rhombus-hosted iframe * **HLS manifests** (`.m3u8`) for direct playback in your own player * **Thumbnail images** for dashboards and quick previews * **Frame captures** at any historical timestamp For downloading recorded footage as MP4 files, see [Recorded Video Access](#recorded-video-access). ## Getting Camera Thumbnails All Rhombus cameras automatically upload thumbnail images. These are perfect for dashboards, monitoring interfaces, and quick status checks. ### Thumbnail Endpoint Retrieve camera thumbnails using a GET request to: ```text theme={null} https://media.rhombussystems.com/media/{cameraUuid}/{mediaRegion}/snapshot.jpeg ``` All `media.rhombussystems.com` endpoints use the same authentication as the main API — include the `x-auth-scheme` and `x-auth-apikey` headers on every request. To get the `cameraUuid` and `mediaRegion` for a camera, call `POST /api/camera/getMinimalCameraStateList`. The response contains a `cameraStates` array; each entry has `uuid` and `mediaRegion` fields. ### Basic Implementation ```bash cURL theme={null} # 1. List cameras to get UUIDs and media regions curl -X POST \ "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ --data '{}' ``` ```bash cURL theme={null} # 2. Fetch the thumbnail using a camera's UUID and media region curl -X GET \ "https://media.rhombussystems.com/media/{CAMERA_UUID}/{MEDIA_REGION}/snapshot.jpeg" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ --output thumbnail.jpg ``` ### Getting Frames at Specific Times To get a JPEG image at a specific historical timestamp, call `POST /api/video/getExactFrameUri`. The response contains a `frameUri` — make a GET request to that URI with the same authentication headers to download the frame. ```javascript theme={null} async function getFrameAtTime(cameraUuid, timestampMs, apiKey) { // Get a signed frame URI const frameUriResponse = await fetch('https://api2.rhombussystems.com/api/video/getExactFrameUri', { method: 'POST', headers: { 'x-auth-scheme': 'api-token', 'x-auth-apikey': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ cameraUuid: cameraUuid, timestampMs: timestampMs // epoch milliseconds }) }); const { frameUri } = await frameUriResponse.json(); // Fetch the actual JPEG const frameResponse = await fetch(frameUri, { headers: { 'x-auth-scheme': 'api-token', 'x-auth-apikey': apiKey } }); return frameResponse.blob(); } ``` The request also accepts optional `downscaleFactor`, `jpgQuality`, and `permyriadCropX`/`permyriadCropY`/`permyriadCropWidth`/`permyriadCropHeight` fields (each in 1/10000ths of the image dimensions, where `10000` = 100%). ## Live Video Streaming Rhombus offers two ways to play live video in your application: 1. **Embed the Rhombus player as an iframe** — easiest path; analytics and timeline events render automatically. 2. **Play the HLS manifest directly** — use your own player (`hls.js`, Safari native, etc.) for full UI control. Both come from the same API call: `POST /api/camera/createSharedLiveVideoStream`. The response contains both URLs: | Field | Description | | --------------------------- | ---------------------------------------------------------- | | `sharedLiveVideoStreamUrl` | Hosted Rhombus player URL — embed in an iframe | | `sharedLiveM3U8StreamUrl` | HLS (`.m3u8`) manifest — play directly with any HLS player | | `sharedLiveVideoStreamUuid` | UUID of the created share, useful for revoking later | ### Embedding Shared Streams as iframes (Recommended) Create a shared stream, then drop the returned URL into an iframe. The iframe player handles authentication via the URL itself, so no API key is needed in the browser. ```javascript theme={null} async function createSharedStream(cameraUuid, apiKey) { const response = await fetch('https://api2.rhombussystems.com/api/camera/createSharedLiveVideoStream', { method: 'POST', headers: { 'x-auth-scheme': 'api-token', 'x-auth-apikey': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ cameraUuid }) }); const data = await response.json(); return data.sharedLiveVideoStreamUrl; } ``` Embed the returned URL: ```html theme={null} ``` ### React Component Example ```jsx theme={null} import React, { useEffect, useState } from 'react'; const RhombusSharedStream = ({ cameraUuid, apiKey, width = "100%", height = "400px", options = {} }) => { const [streamUrl, setStreamUrl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; (async () => { try { setLoading(true); const response = await fetch('https://api2.rhombussystems.com/api/camera/createSharedLiveVideoStream', { method: 'POST', headers: { 'x-auth-scheme': 'api-token', 'x-auth-apikey': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ cameraUuid }) }); if (!response.ok) throw new Error('Failed to create shared stream'); const data = await response.json(); const url = new URL(data.sharedLiveVideoStreamUrl); Object.entries(options).forEach(([key, value]) => { url.searchParams.set(key, value); }); if (!cancelled) { setStreamUrl(url.toString()); setError(null); } } catch (err) { if (!cancelled) setError(err.message); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [cameraUuid, apiKey, options]); if (loading) return
Loading stream...
; if (error) return
Error: {error}
; return (