# Rhombus Developer Documentation > Official documentation for building on the Rhombus platform: the REST API, real-time webhooks and WebSocket events, and implementation guides for cameras, access control, sensors and alarms. --- # Introduction Source: https://developer.rhombus.com > Integrate Rhombus cameras, access control, sensors, and alarm monitoring with your systems through the Rhombus REST API, webhooks, and real-time WebSocket events. ## Quick start 1. **Create an API key** In the [Rhombus Console](https://console.rhombussystems.com/settings/integrations/api-tokens/), go to **Settings → Integrations & Developer Resources → API Tokens** and click **Add API Key**. Give it a **Name**, leave **Auth Type** on **Api Token**, and choose a **Role**: the key can do whatever the role allows, so start with a read-only one. The key is shown only once, so copy it now. [API keys and certificates](https://developer.rhombus.com/authentication/api-keys) covers certificate (mTLS) keys and the other credentials. 2. **Make your first request** List the cameras in your organization. The key goes in two headers, and almost every operation is a `POST` with a JSON body (`{}` when it takes no parameters): ```bash cURL curl -X POST "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: $RHOMBUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```javascript JavaScript const res = await fetch("https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList", { method: "POST", headers: { "x-auth-scheme": "api-token", "x-auth-apikey": process.env.RHOMBUS_API_KEY, "Content-Type": "application/json", }, body: "{}", }); const { cameraStates } = await res.json(); console.log(`Found ${cameraStates.length} cameras`); ``` ```python Python import os import requests res = requests.post( "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList", headers={ "x-auth-scheme": "api-token", "x-auth-apikey": os.environ["RHOMBUS_API_KEY"], }, json={}, ) print(f"Found {len(res.json()['cameraStates'])} cameras") ``` The response lists one entry per camera: ```json { "error": false, "cameraStates": [ { "uuid": "AAAAAAAAAAAAAAAAAAAAAA", "name": "Front Lobby", "locationUuid": "BBBBBBBBBBBBBBBBBBBBBB", "connectionStatus": "GREEN", "healthStatus": "GREEN" } ] } ``` Check `error` as well as the HTTP status: some failures return `200` with `"error": true` and an `errorMsg` ([Errors](https://developer.rhombus.com/errors)). Organizations in the EU (console at `console.eu.rhombussystems.com`) use `https://api2.eu.rhombussystems.com`, and keys work only in their own region; see [API regions](https://developer.rhombus.com/api-regions). 3. **Pick your next step** - **Show video or snapshots in a browser or app:** [Federated tokens](https://developer.rhombus.com/authentication/federated-tokens) and [Streaming video](https://developer.rhombus.com/implementations/streaming-video). - **React to alerts, door events and device changes:** [Webhooks](https://developer.rhombus.com/webhooks) or the [WebSocket](https://developer.rhombus.com/websocket/overview). - **Find the operation you need:** the [API Reference](https://developer.rhombus.com/api-reference/overview), grouped by product. - **Generate a client or connect an AI tool:** [Tools and SDKs](https://developer.rhombus.com/resources). 4. **Go to production** Give each integration its own key with the narrowest role that works ([Permissions and key rotation](https://developer.rhombus.com/authentication/permissions)), handle [rate limits](https://developer.rhombus.com/rate-limits), and see the [Trust Center](https://www.rhombus.com/trust/) for security and compliance. ## Support - **[Rhombus Support](mailto:support@rhombus.com)**: Contact Rhombus Support by email for help with your integration. - **[Knowledge Base](https://support.rhombussystems.com/hc/en-us)**: Setup and administration articles for Rhombus products. - **[System Status](https://status.rhombus.com/)**: Current availability and incident history for Rhombus services. - **[Trust Center](https://www.rhombus.com/trust/)**: Security, data privacy, and compliance information. --- # API Regions (US & EU) Source: https://developer.rhombus.com/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](https://developer.rhombus.com/rate-limits). > **Note**: **API keys and federated tokens are region-bound.** A key generated in the EU Console only works against EU endpoints, and a US key only works against US endpoints. A [federated token](https://developer.rhombus.com/authentication/federated-tokens) works only in the region where it was minted. There is no cross-region redirect — see [Troubleshooting](https://developer.rhombus.com/api-regions#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 → Integrations & Developer Resources → API Tokens**. For example, EU organizations use [console.eu.rhombussystems.com/settings/integrations/api-tokens](https://console.eu.rhombussystems.com/settings/integrations/api-tokens/). Partner accounts use **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 # US organizations BASE_URL="https://api2.rhombussystems.com" # EU organizations # BASE_URL="https://api2.eu.rhombussystems.com" curl -X POST "$BASE_URL/api/org/getOrgV2" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```python Python 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/getOrgV2", headers={ "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json", }, json={}, ) response.raise_for_status() org = response.json()["org"] print(org["uuid"], org["name"]) ``` ```javascript JavaScript // 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/getOrgV2`, { method: "POST", headers: { "Content-Type": "application/json", "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", }, body: JSON.stringify({}), }); const { org } = await response.json(); console.log(org.uuid, org.name); ``` A successful call returns `200 OK` with your organization's details in `org` (for example `org.uuid` and `org.name`). A `401 Unauthorized` with `errorCode` `INVALID_CREDENTIAL` and a `msg` of `Could not find ApiKey[sha256:…]` usually means the key and the base URL belong to different regions. See [Troubleshooting](https://developer.rhombus.com/api-regions#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 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](https://developer.rhombus.com/rhombus-cli) has built-in region support. `rhombus login --region eu` signs in to the EU region and saves the region on the profile; without `--region`, `rhombus login` uses the profile's region. `rhombus configure` asks which region your organization is in at the `Region (us/eu)` prompt and derives the endpoint URL from your answer. Every command then uses the matching regional hosts, including the sign-in and token hosts and, for `rhombus monitor`, the EU WebSocket host. You can also point any single command — or your whole environment — at the EU endpoint directly: ```bash CLI region overrides # 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)](https://developer.rhombus.com/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](https://developer.rhombus.com/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 ### `401 Could not find ApiKey` — but the key is correct A request with a key from the other region fails with `401 Unauthorized`: ```json { "msg": "Could not find ApiKey[sha256:xxxxxxxx]", "error": true, "errorCode": "INVALID_CREDENTIAL", "errorMsg": "The supplied credential is malformed or not recognized.", "status": 401, "timestamp": 1791158465584, "txid": "yJ2kQw8LTaO1xX4hVd7mPA" } ``` This response is identical whether the key was deleted 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. A `msg` of `Invalid api key` (same `errorCode`) means something else: the value isn't a well-formed key (copy-paste damage or whitespace). Quote the `txid` (also in the `x-rhombus-txid` response header) if you contact support. Federated tokens are region-bound too: a token minted on US hosts fails on EU hosts, and the reverse. Mint it through the API host in the same region as the cameras you'll stream. ### 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 - **[API Reference](https://developer.rhombus.com/api-reference/overview)**: Browse 1,000+ endpoints — every path works identically in both regions. - **[Rhombus CLI](https://developer.rhombus.com/rhombus-cli)**: Configure the CLI for your region and explore the API from your shell. - **[WebSocket Events](https://developer.rhombus.com/websocket/overview)**: Stream real-time events from your region's WebSocket endpoint. - **[Rate Limits](https://developer.rhombus.com/rate-limits)**: The same limits apply in both regions, enforced within your organization's region. --- # Errors and Status Codes Source: https://developer.rhombus.com/errors > How the Rhombus REST API reports errors: HTTP status codes, the error body and its errorCode, authentication and permission errors, the error field on 200 responses, validation and JSON errors, server errors, and which failures to retry. The REST API reports a failed request in one of two ways, so check both: 1. **An HTTP error status** when the request itself can't be processed: malformed JSON, a missing field, a credential problem, a permission denial or a rate limit. The body has `"error": true` and a machine-readable `errorCode`. 2. **HTTP `200` with `"error": true`** when the request was valid but the action failed or was refused, for example a role you're not allowed to assign. The reason is in `errorMsg`: ```json { "error": true, "errorMsg": "User not permitted to generate an api token for the specified role" } ``` A request succeeded only when the status is `2xx` **and** `error` is `false`. ## The response envelope Every JSON response from the REST API carries the same three fields alongside its data: | Field | Present | Meaning | | ------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | `error` | Always | `false` on success, `true` when the action failed | | `errorMsg` | When `error` is `true` | A human-readable reason. Log it and show it to an operator; don't parse it, because the wording can change | | `warningMsg` | Rarely | The request succeeded, but something needs attention, such as a default the server applied for you | Error responses have the same body on every host: the REST API, the media hosts, WebSocket handshakes and the MCP server. Fields whose value is null are left out of responses, so treat a missing field as null. Empty lists are still sent. ## Status codes | Status | Body | Meaning | Retry? | | -------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `200` with `"error": true` | `{"error":true,"errorMsg":"…", …}` | The action failed or was refused | No; act on `errorMsg` | | `400` | `{"msg":"Invalid Json", …}` | The body isn't valid JSON | No | | `400` | `{"msg":"JSON doesn't match expected object structure", …}` | A field has the wrong type, an enum value isn't recognized, or (on a few operations) the body has a field the operation doesn't accept | No | | `400` | `{"msg":"Request missing required field", …}`, or `{"error":true,"errorMsg":"[Error: [field]: …]", …}` | A required field is missing or a value is out of range. The message names the field | No | | `400` | `{"errorCode":"INVALID_AUTH_ORG", …}` | The `x-auth-org` header is malformed | No | | `401` | `{"errorCode":"UNAUTHENTICATED", …}` or another credential code | The credential is missing, malformed, unknown, expired or revoked | No; see [Authentication and permission errors](https://developer.rhombus.com/errors#authentication-and-permission-errors) | | `403` | `{"errorCode":"PERMISSION_REQUIRED", …}` or another permission code | The credential is valid, but it isn't permitted to do this | No; see [Authentication and permission errors](https://developer.rhombus.com/errors#authentication-and-permission-errors) | | `404` | `{"msg":"Not Found", …}` | The `/api` path doesn't exist (`NOT_FOUND`; paths are case-sensitive, so a path with the wrong capitalization gets `404` too), or something the operation looks up doesn't exist | No | | `405` | `{"errorCode":"METHOD_NOT_ALLOWED", …}` | The path doesn't accept this HTTP method | No | | `415` | `{"errorCode":"UNSUPPORTED_MEDIA_TYPE", …}` | The request's `Content-Type` isn't one the operation accepts | No | | `429` | `{"errorCode":"RATE_LIMITED", …}`, or an `errorMsg` naming the cameras | Your organization's rate limit, or a per-camera limit on clips and frames | Yes, after `Retry-After`; see [Rate Limits](https://developer.rhombus.com/rate-limits) | | `500` | `{"msg":"…", …}` | An unexpected server error | Yes, a few times with backoff | | `503` | `{"errorCode":"SERVICE_UNAVAILABLE", …}` | Rhombus couldn't check the credential at that moment | Yes, with backoff | | `502`, `503`, `504` | Varies | A temporary gateway or availability problem | Yes, with backoff | Most refusals arrive as `200` with `"error": true`; only a few operations answer `404`. Handle both. ## Authentication and permission errors `401` means Rhombus couldn't establish who is calling. `403` means it could, and that caller isn't allowed to do this. Neither succeeds if you repeat it unchanged: fix the credential, the role or the request. The one case to retry is an expired credential, after you renew it. | Status | `errorCode` | Cause | | ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | `UNAUTHENTICATED` | No credential was presented | | `401` | `INVALID_CREDENTIAL` | The credential is malformed or isn't recognized, including a deleted API key or a revoked federated token | | `401` | `CREDENTIAL_EXPIRED` | The credential has expired. Refresh the OAuth access token, mint a new federated token or replace the API key, then retry | | `401` | `CREDENTIAL_REVOKED` | The OAuth grant behind the access token was revoked | | `401` | `CERTIFICATE_MISMATCH` | The client certificate doesn't belong to the API key (`Client certificate does not match the api key`) | | `403` | `PERMISSION_REQUIRED` | The credential's role, or an OAuth access token's scopes, don't allow this operation or device | | `403` | `PARTNER_CLIENT_ACCESS_DENIED` | The partner credential can't act in the organization named in `x-auth-org` | | `403` | `DOMAIN_NOT_ALLOWED` | A federated token minted with a `domain` came from another site: the request's `Origin`, or its `Referer` when there's no `Origin`, doesn't match | | `403` | `DEVICE_SCOPE_MISMATCH` | A device-scoped federated token was used for another device, or for a request that isn't tied to its device | **Missing OAuth scope.** When an OAuth access token lacks the scope an operation needs, the `403` names the scope in a `WWW-Authenticate` header. Ask for that scope when the user signs in ([Sign in with Rhombus](https://developer.rhombus.com/oauth-authentication)): ```text Missing scope HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", scope="physical" ``` **Password-protected share links.** A request to a password-protected share link without the right password gets `401` with `WWW-Authenticate: Basic`. [Authentication](https://developer.rhombus.com/authentication#reading-authentication-errors) lists what to check for each kind of credential. ## Request bodies - **Send `POST` with `Content-Type: application/json`** and a JSON object, `{}` when the operation takes no parameters. - **Field names are case-sensitive, and most operations ignore fields they don't recognize.** A misspelled or wrongly capitalized field is dropped without an error, so a filter that seems to have no effect is usually a spelling mistake. Copy field names from the [API Reference](https://developer.rhombus.com/api-reference/overview). - **Enum values are case-sensitive.** An unrecognized value is usually rejected with `400`, but a few fields replace it with their default value instead, so a typo can change a setting. Send values exactly as the reference lists them. - **Timestamps are Unix epoch milliseconds** in fields ending in `Ms`. Fields ending in `Sec`, `Secs` or `Seconds` are in seconds. - **Validation messages use the server's field names**, which can have a leading underscore: `[Error: [_targetUuid]: unexpected null value for required field]` refers to `targetUuid`. ## Retries and backoff Retry only what can succeed unchanged: | Response | Action | | ------------------------------------------------------------- | ----------------------------------------------------------------------- | | `2xx` with `"error": false` | Success | | `2xx` with `"error": true` | Don't retry unchanged; fix the request or the data `errorMsg` describes | | `400`, `401`, `403`, `404`, `405`, `415` | Don't retry; fix the request, the credential or the role | | `401` with `CREDENTIAL_EXPIRED` | Renew the credential, then retry | | `429` | Wait the `Retry-After` seconds, then retry | | `500`, `502`, `503`, `504`, a timeout or a dropped connection | Retry with exponential backoff and jitter | How to wait: - **Throttling (`429`)**: the server says how long in `Retry-After`, for both your [organization's limit](https://developer.rhombus.com/rate-limits) and the [per-camera limits](https://developer.rhombus.com/rate-limits#per-camera-limits-on-clips-and-frames) on clips and frames. Wait at least that long, plus a little random jitter so many workers don't retry at the same instant. Retrying a throttled camera early makes its next wait longer. - **Temporary failures (`5xx`, network errors)**: back off exponentially with full jitter: wait a random time between 0 and `min(60 s, 1 s × 2^attempt)`. - **Bound every retry loop** with a maximum number of attempts (about 5) and a time budget, then report the failure with its status, `errorCode`, `errorMsg` and `txid`. - **Check before repeating a write.** If a request that creates something (a clip, a key, a user) timed out or failed with a `5xx`, it may still have succeeded; look for the result before sending it again, so you don't create a duplicate. - **Share the budget.** Every key and token in your organization draws on the same rate limit, so many workers retrying at once still count together; spread scheduled jobs out instead of starting them all on the minute. A client wrapper that follows these rules: ```python Python import random import time import requests RETRYABLE = {429, 500, 502, 503, 504} session = requests.Session() session.headers.update({"x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY"}) def call(url, body, attempts=5, base=1.0, cap=60.0, budget=120.0): """POST to the Rhombus API, retrying throttling and temporary failures.""" deadline = time.monotonic() + budget for attempt in range(attempts): try: response = session.post(url, json=body, timeout=30) except (requests.ConnectionError, requests.Timeout): response = None # network trouble: retry like a 5xx if response is not None and response.status_code not in RETRYABLE: response.raise_for_status() # other 4xx: fix the request, don't retry data = response.json() if data.get("error"): raise RuntimeError(data.get("errorMsg")) # refused: don't retry unchanged return data if response is not None and response.status_code == 429: delay = float(response.headers.get("Retry-After", "1")) + random.uniform(0, 1) else: delay = random.uniform(0, min(cap, base * 2 ** attempt)) if attempt == attempts - 1 or time.monotonic() + delay > deadline: break time.sleep(delay) raise RuntimeError(f"gave up after {attempt + 1} attempts: {url}") ``` ```javascript JavaScript const RETRYABLE = new Set([429, 500, 502, 503, 504]); const headers = { "x-auth-scheme": "api-token", "x-auth-apikey": "YOUR_API_KEY", "Content-Type": "application/json" }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export async function call(url, body, { attempts = 5, baseMs = 1000, capMs = 60000, budgetMs = 120000 } = {}) { const deadline = Date.now() + budgetMs; for (let attempt = 0; attempt < attempts; attempt++) { let response; try { response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(30000) }); } catch { response = undefined; // network trouble: retry like a 5xx } if (response && !RETRYABLE.has(response.status)) { if (!response.ok) throw new Error(`HTTP ${response.status}`); // other 4xx: fix the request const data = await response.json(); if (data.error) throw new Error(data.errorMsg); // refused: don't retry unchanged return data; } const delay = response?.status === 429 ? Number(response.headers.get("Retry-After") ?? 1) * 1000 + Math.random() * 1000 : Math.random() * Math.min(capMs, baseMs * 2 ** attempt); if (attempt === attempts - 1 || Date.now() + delay > deadline) break; await sleep(delay); } throw new Error(`gave up: ${url}`); } ``` ## Contacting support When an error persists, email with: - The operation path and the time of the request, with its time zone. - Your region (US or EU). - The status code and the body, including `errorCode` and `errorMsg`. - The request ID: `txid` in the error body, also sent as the `x-rhombus-txid` response header. Never send your API key, OAuth tokens or federated tokens. --- # Pagination and Time Ranges Source: https://developer.rhombus.com/pagination > How Rhombus list operations return results in pages: lastEvaluatedKey cursors, page tokens, timestamp cursors and time windows, with loops in Python and JavaScript and the pitfalls that cause missed or duplicated results. Operations that can return many results page them in one of a few styles. Each operation's [API Reference](https://developer.rhombus.com/api-reference/overview) page shows which fields it takes; this page explains how each style works. | Style | Request fields | Response field | Last page when | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -------------------------- | ------------------------------------ | | [Cursor](https://developer.rhombus.com/pagination#cursor-pages) | `maxPageSize`, `lastEvaluatedKey` (sometimes inside `pageRequest`) | `lastEvaluatedKey` | `lastEvaluatedKey` is absent | | [Page token](https://developer.rhombus.com/pagination#page-tokens) | `pageSize`, `pageToken` | `pageToken` | `pageToken` is absent | | [Timestamp cursor](https://developer.rhombus.com/pagination#timestamp-cursors) | `maxPageSize`, `pageTimestampMs` | `lastEvaluatedTimestampMs` | `lastEvaluatedTimestampMs` is absent | | [Time window](https://developer.rhombus.com/pagination#time-windows) | `createdAfterMs`, `createdBeforeMs`, `limit` | none | A page has fewer than `limit` events | | [Last item](https://developer.rhombus.com/pagination#policy-alerts) | `maxResults`, `lastTimestampMs`, `lastUuid` | none | A page comes back empty | All timestamps are Unix epoch milliseconds. ## Cursor pages Most list operations take `maxPageSize` and `lastEvaluatedKey`, and return `lastEvaluatedKey` while more results remain. Send it back unchanged to get the next page; when the response has no `lastEvaluatedKey`, you have everything. For example, [`findAccessControlledDoors`](https://developer.rhombus.com/api-reference/component-webservice/find-access-controlled-doors-within-the-org): ```python Python import os import requests URL = "https://api2.rhombussystems.com/api/component/findAccessControlledDoors" HEADERS = {"x-auth-scheme": "api-token", "x-auth-apikey": os.environ["RHOMBUS_API_KEY"]} doors, key = [], None while True: body = {"maxPageSize": 100} if key: body["lastEvaluatedKey"] = key page = requests.post(URL, headers=HEADERS, json=body).json() if page.get("error"): raise RuntimeError(page.get("errorMsg")) doors += page.get("accessControlledDoors", []) key = page.get("lastEvaluatedKey") if not key: break ``` ```javascript JavaScript const url = "https://api2.rhombussystems.com/api/component/findAccessControlledDoors"; const headers = { "x-auth-scheme": "api-token", "x-auth-apikey": process.env.RHOMBUS_API_KEY, "Content-Type": "application/json", }; const doors = []; let key; do { const body = { maxPageSize: 100, ...(key && { lastEvaluatedKey: key }) }; const page = await (await fetch(url, { method: "POST", headers, body: JSON.stringify(body) })).json(); if (page.error) throw new Error(page.errorMsg); doors.push(...(page.accessControlledDoors ?? [])); key = page.lastEvaluatedKey; } while (key); ``` Some operations nest the same two fields in a `pageRequest` object (`{"pageRequest": {"maxPageSize": 100, "lastEvaluatedKey": "…"}}`) and return `lastEvaluatedKey` at the top level of the response. - **Keep going until `lastEvaluatedKey` is absent.** A page can hold fewer results than `maxPageSize`, or none at all, while more remain: results you aren't allowed to see are removed after the page is read. - **Always set `maxPageSize`.** Without it, some operations return every result in one response, and others apply their own default. Each operation has its own maximum, and a larger value is rejected with `400`. - **Treat `lastEvaluatedKey` as opaque.** Don't build, edit or store it long-term. A value the server can't read isn't rejected: the operation starts again from the first page. - **Keep the other fields the same** on every page of one listing. ## Page tokens A few operations, such as [`getClipsWithProgress`](https://developer.rhombus.com/api-reference/event-webservice/get-list-of-saved-clips-in-organization-with-current-progress), use `pageSize` and `pageToken` instead. Send the `pageToken` from each response with the next request, and stop when the response has none. For `getClipsWithProgress`, `pageSize` is 1 to 200 and defaults to 100. ## Timestamp cursors [`getSavedClipsV2`](https://developer.rhombus.com/api-reference/event-webservice/get-list-of-saved-clips-in-organization) returns `lastEvaluatedTimestampMs`. Send it as `pageTimestampMs` in the next request, and stop when the response doesn't include it. Set `maxPageSize`; it defaults to 100. As with cursors, a page can be short while more clips remain. ## Time windows Event history for doors and sensors, such as [`findComponentEventsByAccessControlledDoor`](https://developer.rhombus.com/api-reference/component-webservice/retrieve-all-component-events-relevant-to-the-specified-accesscontrolleddoor), takes a window and a count instead of a cursor: - `createdAfterMs` and `createdBeforeMs` bound the window. `createdBeforeMs` is exclusive. - `limit` caps the number of events, the most recent first. **Set it:** without `limit`, every event in the window comes back in one response. To read further back, repeat the request with `createdBeforeMs` set to one millisecond after the oldest event you received, and drop events you already have by their UUID. Moving the boundary to exactly the oldest timestamp can skip events that share that millisecond. ## Policy alerts [`getPolicyAlerts`](https://developer.rhombus.com/api-reference/event-webservice/get-recent-policy-alerts-for-organization) returns alerts newest first, up to `maxResults`, with no cursor in the response. For the next page, send the `timestampMs` and `uuid` of the last alert you received as `lastTimestampMs` and `lastUuid`, keeping the same filters and window (`afterTimestampMs`, `beforeTimestampMs`). Stop when a page comes back empty or you reach the start of your window. ## Choosing a page size Larger pages mean fewer requests against your [rate limit](https://developer.rhombus.com/rate-limits), but slower responses. For a full sync of a large organization, use the largest size the operation accepts, and keep the loop going until the last-page condition in the table above. --- # Rate Limits Source: https://developer.rhombus.com/rate-limits > Understand Rhombus API rate limits and build resilient integrations that handle 429 throttled responses with exponential backoff and retry logic. REST API requests count against a rate limit when they're made with any of these credentials: - [API keys](https://developer.rhombus.com/authentication/api-keys), including executable and chatbot keys - [Partner credentials](https://developer.rhombus.com/partner-api-calls): partner keys, certificates and partner OAuth access tokens - [OAuth access tokens](https://developer.rhombus.com/oauth-authentication) - [Federated tokens](https://developer.rhombus.com/authentication/federated-tokens) The limit is **per organization**: all of these credentials acting in your organization share one budget, with the same limits, so creating more credentials doesn't raise it. A partner credential's requests count against the client organization named in `x-auth-org`, or the partner organization when the header is absent. ## How limits work Rhombus uses a **token bucket** algorithm. Two values govern your throughput: - A **sustained refill rate** (requests per second) — your steady-state ceiling. - A **burst capacity**, roughly 10× the refill rate by default — headroom that absorbs short spikes. Bursts succeed instantly while the bucket has tokens. Once the bucket drains, sustained traffic above the refill rate returns `429`s until the bucket refills. Default rates apply to all organizations, and the same limits apply in both the US and EU [regions](https://developer.rhombus.com/api-regions) — they're enforced within your organization's region. To request a higher limit, contact [Rhombus support](mailto:support@rhombus.com). ## Throttled responses Two kinds of limit answer with `429 Too Many Requests` and a `Retry-After` header, the number of seconds to wait (at least 1): | Limit | Applies to | Body | | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Organization rate limit | Every REST request made with one of the credentials above | The JSON [error body](https://developer.rhombus.com/errors#the-response-envelope) with `"errorCode": "RATE_LIMITED"` | | [Per-camera limits](https://developer.rhombus.com/rate-limits#per-camera-limits-on-clips-and-frames) | Creating clips and fetching exact frames, from any client | JSON with `"error": true` and an `errorMsg` naming the cameras | ```text Organization rate limit HTTP/1.1 429 Too Many Requests Retry-After: 1 Content-Type: application/json {"error":true,"errorCode":"RATE_LIMITED","errorMsg":"…","status":429,"txid":"…", …} ``` ```text Per-camera limit HTTP/1.1 429 Too Many Requests Retry-After: 12 Content-Type: application/json {"error":true,"errorMsg":"Clip creation is temporarily throttled for device(s) [AAAAAAAAAAAAAAAAAAAAAA]; retry after 12 second(s)"} ``` For the organization limit, `Retry-After` comes from your organization's refill rate. Branch on the status code or `errorCode`, and wait the seconds in `Retry-After`. > **Warning**: Do **not** immediately retry on a `429`. Repeated requests while rate limited continue to be rejected and do not reset your limit window. ## Per-camera limits on clips and frames Clip creation ([`spliceV3`](https://developer.rhombus.com/api-reference/video-webservice/splice-v3) and the older `splice` and `spliceV2`) and exact frames ([`getExactFrameUri`](https://developer.rhombus.com/api-reference/video-webservice/get-exact-frame-uri), [`getExactFrameData`](https://developer.rhombus.com/api-reference/video-webservice/get-exact-frame-data) and [`getLiveImage`](https://developer.rhombus.com/api-reference/video-webservice/get-live-image)) also have a limit per camera, separate from your organization's limit and shared by everything that uses that camera, including the console: - Each camera allows a burst of requests, then a slower sustained rate. - A rejection starts a waiting period, and requests during it are rejected too. Rejections after it ends make the next wait longer, so retrying early only delays you. - A request that names several cameras is rejected as a whole if any of them is over its limit, and `Retry-After` is the longest wait. To export or sample a lot of footage, spread requests across cameras and over time instead of sending them for one camera back to back. ## OAuth client registration Anonymous dynamic client registration (`POST /oauth/register`) has its own limits: 10 registrations per hour from one client IP address, and 100 per hour overall. Over either limit, registration answers `429` with `Retry-After` and an OAuth error body, where `error` is the string `temporarily_unavailable`. See [Sign in with Rhombus](https://developer.rhombus.com/oauth-authentication). ## Handling rate limits 1. **Read the Retry-After header** The `Retry-After` header tells you exactly how many seconds to wait, computed from your organization's refill rate. Always prefer this value over hardcoded delays. 2. **Pause requests** Stop sending requests for the duration specified in the header. 3. **Retry your request** After the wait period, retry the original request. ## Retry strategy Wait the `Retry-After` seconds on a `429`, and back off exponentially with full jitter on temporary server errors: ```text Backoff delay = random(0, min(max_delay, base_delay × 2^attempt)) ``` | Parameter | Recommended value | | ------------ | ----------------------------- | | Base delay | 1 second | | Max delay | 60 seconds | | Max attempts | About 5, within a time budget | [Errors: retries and backoff](https://developer.rhombus.com/errors#retries-and-backoff) has a client wrapper in Python and JavaScript that applies these rules, and says which responses not to retry. ## When to retry Retry `429`, `5xx` responses and network failures; don't retry other `4xx` responses or `200` responses with `"error": true`. [Errors](https://developer.rhombus.com/errors#retries-and-backoff) explains each case. ## Alert notification throttling Alert notifications (distinct from API rate limits) have a configurable **minimum interval** between consecutive alerts of the same type for a given device. This prevents alert fatigue from high-frequency events like motion detection. | Interval | Seconds | | ----------------------- | ------- | | 1 minute | 60 | | 2 minutes | 120 | | **5 minutes (default)** | 300 | | 10 minutes | 600 | | 20 minutes | 1200 | | 30 minutes | 1800 | | 1 hour | 3600 | Configure this per policy and per activity type. During the backoff window, duplicate alerts for the same device and activity type are suppressed server-side. > **Tip**: Alerts with **new identity information** — such as a different recognized face or license plate — bypass the backoff and are delivered immediately, even within the suppression window. --- # Versioning and Deprecation Source: https://developer.rhombus.com/versioning > How the Rhombus REST API changes over time: new operation versions such as spliceV3, deprecated operations and their replacements, Deprecation and Sunset headers, changes made without a new version, and how to write clients that keep working. The Rhombus REST API has a single version. There's no version header and no version in the URL: every operation lives at `/api//`. ## New versions of an operation When an operation has to change in a way that would break existing clients, Rhombus adds a new operation with a version suffix and keeps the old one. For example, [`spliceV3`](https://developer.rhombus.com/api-reference/video-webservice/splice-v3) sits alongside `splice` and `spliceV2`, and [`getSavedClipsV2`](https://developer.rhombus.com/api-reference/event-webservice/get-list-of-saved-clips-in-organization) alongside `getSavedClips`. Use the highest version that exists unless the reference says otherwise. ## Deprecated operations The old operation is then marked deprecated: - The [API Reference](https://developer.rhombus.com/api-reference/overview) labels it **Deprecated** and, when there is one, links to the replacement. The sidebar hides deprecated operations unless you turn on **Show deprecated operations**. - The [OpenAPI specification](https://api2.rhombussystems.com/api/openapi/public.json) marks it `deprecated: true`, so generated clients flag it too. Deprecated operations keep working, but they get no new features. Move to the replacement when you next work on that part of your integration. If an operation is eventually retired, it answers with `"error": true` and an `errorMsg` saying so; see [Errors](https://developer.rhombus.com/errors). ## Deprecation and Sunset headers When a way of calling the API is scheduled to stop working, responses to requests that still use it carry two headers: ```text Deprecated request Deprecation: true Sunset: Tue, 05 Jan 2027 00:00:00 GMT ``` - `Deprecation: true` means the request relied on something deprecated. The request itself still succeeds. - `Sunset` is the date after which the same request fails with an error. For example, a [partner credential](https://developer.rhombus.com/partner-api-calls) that calls an endpoint outside `/api/partner/` without the `x-auth-org` header gets these headers, with the date above; after that date, the same request is an error. On the event WebSocket, such a connection is refused with 403 `PERMISSION_REQUIRED`. Send `x-auth-org` with the UUID of the organization to act in ([Scoping a call to a client organization](https://developer.rhombus.com/partner-api-calls#scoping-a-call-to-a-client-organization)). Log every response that carries `Deprecation`, with the operation and the `Sunset` date, so you find these calls before the date. ## Changes without a new version These changes can appear in the existing API at any time; notable ones are announced in the [Changelog](https://developer.rhombus.com/changelog): - New operations. - New optional fields in requests. - New fields in responses. - New values in enums, such as a new alert or event type. Write clients that tolerate them: - **Ignore response fields you don't use**, and don't fail on ones you don't recognize. Most JSON libraries do this by default; strict deserializers (for example Jackson with `FAIL_ON_UNKNOWN_PROPERTIES`) need it turned off. - **Handle enum values you don't know**, for example by logging them and skipping the item, rather than throwing. - **Treat a missing field as null.** Fields with null values are left out of responses. - **Don't depend on the order** of fields in a response or of items in a list, unless the operation documents an order. ## Staying informed - The [Changelog](https://developer.rhombus.com/changelog) lists notable changes, with an [RSS feed](https://developer.rhombus.com/changelog/rss.xml). - The OpenAPI specification's `info.version` doesn't change between releases. To see exactly what changed, keep a copy of the specification and compare it with the current one. --- # Security and Trust Source: https://developer.rhombus.com/security > How Rhombus secures its developer platform and what your integration is responsible for: reporting vulnerabilities, compliance, data residency, transport security, network endpoints and ports, and a security checklist for integrations. Rhombus secures the platform your integration calls; your integration secures its credentials, its endpoints and what it does with the data. This page covers both sides, and where to find Rhombus's compliance documentation. ## Report a vulnerability Email with what you found, the steps to reproduce it, and its impact. Test only against your own organization, and don't access or change other customers' data. The same contact is published in [security.txt](https://developer.rhombus.com/.well-known/security.txt), and the policy is on the [Trust Center](https://www.rhombus.com/trust/). For an exposed credential of your own, such as an API key committed to a public repository, don't wait: delete the key in the console and create a new one ([Rotate an API key](https://developer.rhombus.com/authentication/permissions#rotate-an-api-key)). Deleting a key or revoking an OAuth grant takes effect within 60 seconds on every host, and open WebSocket and media connections that use it are closed. Federated tokens minted with that key or grant stop working too, and a single leaked federated token can be revoked on its own ([Federated tokens](https://developer.rhombus.com/authentication/federated-tokens)). ## Compliance Rhombus's security and compliance program covers SOC 2, NDAA, GDPR and TAA. Reports, certificates, and security and privacy documentation are on the [Trust Center](https://www.rhombus.com/trust/). ## Data residency Every organization lives in one region, US or EU. An organization provisioned in the EU is hosted in the European Union: its API traffic, video storage and cloud processing stay in the region. Credentials are region-bound, so a US key or token doesn't work on EU hosts. [API regions](https://developer.rhombus.com/api-regions) lists each region's hosts. ## How this site measures usage This site records anonymous usage to improve the documentation: the pages read, searches and the results chosen, questions asked to Ask AI, and the API operations sent with Try it (the operation and its status only, never parameters, bodies or keys). It sets no cookies and keeps nothing that identifies you. Anything that looks like a key or an email address in what you type is removed before it's stored, and the data stays in Rhombus's own systems. ## Transport security Every Rhombus API, media and WebSocket endpoint uses TLS: media is served over HTTPS and secure WebSockets. Cameras on your local network serve their streams over HTTPS on port 8000, at hostnames such as `192-168-1-50.lan.rhombussystems.com` that resolve to the camera's local address. ## Network endpoints To allow an integration through a firewall, open outbound HTTPS to these hosts (EU organizations use the `.eu.` hosts listed in [API regions](https://developer.rhombus.com/api-regions)): | Purpose | Host | Port | | ------------------------------------ | -------------------------------------------------------------------------------- | ---- | | REST API | `api2.rhombussystems.com` | 443 | | Snapshots, frames and clip downloads | `media.rhombussystems.com` | 443 | | Live and recorded streams | Per-camera hosts under `dash.rhombussystems.com`, returned by `getMediaUris` | 443 | | Event WebSocket | `ws.rhombussystems.com` | 8443 | | OAuth sign-in and tokens | `auth-web.rhombussystems.com` | 443 | | Cameras on your local network | `
.lan.rhombussystems.com`, which resolves to the camera's local address | 8000 | Webhooks travel the other way: Rhombus connects to your HTTPS endpoint. Authenticate them by their [signature](https://developer.rhombus.com/webhooks#verifying-webhook-signatures), not by source address. ## Your integration's responsibilities - **Keep API keys on servers.** Browsers, mobile apps and anything users can inspect get short-lived [federated tokens](https://developer.rhombus.com/authentication/federated-tokens), scoped to a device where possible and bound to your site's origin with `domain` when they're used in a browser. - **Give each integration its own key with the narrowest role** that works, and rotate keys on a schedule and whenever one may have leaked ([Permissions and key rotation](https://developer.rhombus.com/authentication/permissions)). - **Verify every webhook** before acting on it, and serve your receiver over HTTPS ([Webhook receivers](https://developer.rhombus.com/implementations/webhook-listener)). - **Put a person in the loop for physical actions.** Unlocking doors, lockdowns and alarm escalation deserve their own key and, when triggered by automation or an AI assistant, a human approval step. An OAuth app that doesn't need them leaves out the `physical` scope ([Sign in with Rhombus](https://developer.rhombus.com/oauth-authentication)). - **Treat data from Rhombus as sensitive.** Payloads, footage and tokens name people and places: keep them out of shared logs and analytics, and apply your own retention rules. - **Watch the audit log.** Actions taken with an API key are recorded under the key's name ([Audit trail](https://developer.rhombus.com/authentication/permissions#audit-trail)). --- # Authentication Source: https://developer.rhombus.com/authentication > How to authenticate every kind of Rhombus request: which credential to use for REST, media and video, the event WebSocket and cameras on your network, where each one goes, partner credentials, and how to read 401, 403 and 429 responses. Every request to Rhombus carries a credential. Which credentials a request can use, and where they go, depends on **where the request goes**: the REST API, the media and video hosts, the event WebSocket, or a camera on your local network. This page maps each credential to each destination and explains the authentication errors; the pages after it cover each credential in detail. ## Choose a credential | You're building | Use | Details | | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | A backend service, script or scheduled job in one organization | **API key** | [API keys and certificates](https://developer.rhombus.com/authentication/api-keys) | | A backend whose security policy requires mutual TLS | **Certificate API key** (mTLS) | [API keys and certificates](https://developer.rhombus.com/authentication/api-keys#certificate-keys-mtls) | | An app that other Rhombus customers sign in to and authorize | **OAuth access token** ("Sign in with Rhombus") | [Sign in with Rhombus](https://developer.rhombus.com/oauth-authentication) | | A tool for an MSP or reseller that manages many client organizations | **Partner API key** or partner OAuth token | [Partner API](https://developer.rhombus.com/partner-api-calls) | | Video, images, audio or live events in a browser or a mobile app, or anything on a camera's local network | **Federated token**, minted by your backend | [Federated tokens](https://developer.rhombus.com/authentication/federated-tokens) | | A link anyone can open to watch one camera | **Shared stream link** | [Media and streams](https://developer.rhombus.com/authentication/media#shared-stream-links) | > **Warning**: API keys and partner keys are server-side credentials. Never put one in a web page, a mobile app or any code a user can read. Anything that runs on a user's device gets a short-lived [federated token](https://developer.rhombus.com/authentication/federated-tokens) from your backend instead. ## Where each credential works | Credential | REST API (`api2`) | Media and video | Event WebSocket (`ws`) | Cameras on your LAN | | ------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------- | ---------------------------------- | --------------------------- | | **API key** | Headers | Headers | Handshake headers | Not accepted | | **Certificate API key** | Client certificate plus headers (`api2` hosts only) | Not accepted; use an API key or a federated token | Not accepted | Not accepted | | **OAuth access token** | Headers | Headers | Handshake headers | Not accepted | | **Partner key, certificate or OAuth token** | Same as the organization credential, plus `x-auth-org` | Same, plus `x-auth-org` | Same, plus `x-auth-org` (required) | Not accepted | | **Federated token** | Headers or query parameters | Headers or query parameters | Headers or query parameters | Headers or query parameters | Cameras on your local network accept **federated tokens only**. That's deliberate: on the local network, every request carries a short-lived token that you can limit to the one camera it needs, never a long-lived key. See [Streaming over LAN](https://developer.rhombus.com/implementations/lan-streaming). Hosts per destination ([US & EU Regions](https://developer.rhombus.com/api-regions) lists the EU variants): | Destination | US host | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | REST API | `https://api2.rhombussystems.com` | | Media and video | `https://media.rhombussystems.com`, plus the per-camera streaming hosts that [`getMediaUris`](https://developer.rhombus.com/api-reference/camera-webservice/get-camera-media-uris) returns | | Event WebSocket | `wss://ws.rhombussystems.com:8443/websocket` | | Cameras on your LAN | `https://.lan.rhombussystems.com:8000`, returned by `getMediaUris` | ## Request headers A request names its **scheme** in `x-auth-scheme` and carries the credential in the header that scheme reads: | Credential | `x-auth-scheme` | Credential header | Extra | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | API key | `api-token` | `x-auth-apikey` | — | | Certificate API key | `api` | `x-auth-apikey` | TLS client certificate | | OAuth access token | `api-oauth-token`, or leave it out | `x-auth-access-token`, or `Authorization: Bearer ` | — | | Federated token | `federated-token` | `x-auth-ft` | Both may be query parameters instead: `?x-auth-scheme=federated-token&x-auth-ft=` | | Partner key, certificate or OAuth token | The same three schemes; `partner-api-token`, `partner-api` and `partner-api-oauth-token` are accepted as aliases | The same headers | `x-auth-org: ` (see [Partner credentials](https://developer.rhombus.com/authentication#partner-credentials)) | ```bash API key curl -X POST "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: $RHOMBUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```bash OAuth access token curl -X POST "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` Rules that apply everywhere: - **Header names and the scheme value are exact.** `x-auth-scheme: api-token` works; `Api-Token` does not. - **`Authorization: Bearer` carries an OAuth access token only**, on the REST API, the media hosts and the event WebSocket. A Bearer header with no `x-auth-scheme` is read as an OAuth access token. API keys never go in `Authorization`. - **Only federated tokens go in a URL.** `x-auth-scheme` and `x-auth-ft` may be query parameters; API keys, OAuth access tokens and `x-auth-org` are read from headers only. - **Credentials are region-bound.** A key or token from a US organization doesn't work on EU hosts, and the reverse. The error doesn't mention the region. See [US & EU Regions](https://developer.rhombus.com/api-regions). - **Lifetimes:** an API key works until you delete it or until its expiry (`expiresAtMs`), if it has one. OAuth access tokens last one hour. Federated tokens last the `durationSec` you ask for when you mint them: one hour by default, at most 24 hours. - **Revocation takes effect within 60 seconds on every host.** Deleting a key, changing its role or revoking an OAuth grant ends its access, and open WebSocket connections that used it are closed. - **Send `Content-Type: application/json`** on every POST to the REST API, even with an empty body (`{}`). ## Partner credentials Partner keys, partner certificate keys and partner OAuth tokens use the same schemes as the organization credentials. To act in a client organization, send that organization's UUID in `x-auth-org`: - `x-auth-org` is a **header only**; it's never read from a query parameter. It means the same for partner keys, certificates and OAuth tokens, and an empty value counts as absent. - Outside the `/api/partner/` endpoints, **always send `x-auth-org`**. A partner request without it still works, but the response carries `Deprecation: true` and `Sunset: Tue, 05 Jan 2027 00:00:00 GMT`, and after that date the request fails. The event WebSocket refuses a partner handshake without `x-auth-org` with 403 `PERMISSION_REQUIRED`. - A malformed value returns 400 `INVALID_AUTH_ORG`; a client organization your partner account doesn't manage returns 403 `PARTNER_CLIENT_ACCESS_DENIED`. See [Partner API](https://developer.rhombus.com/partner-api-calls) for listing clients and calling into them. ## Reading authentication errors Every error response from the REST API, the media hosts and the WebSocket handshakes has the same JSON body, and carries the request ID in the `x-rhombus-txid` header too: ```json { "msg": "Unauthorized", "error": true, "errorCode": "CREDENTIAL_EXPIRED", "errorMsg": "The supplied credential has expired.", "status": 401, "timestamp": 1792371600000, "txid": "yJ2kQw8LTaO1xX4hVd7mPA" } ``` Branch on the status and `errorCode`, show or log `errorMsg`, and quote `txid` when you contact support. The status says which part of the request to fix: - **401**: Rhombus couldn't establish who is calling. The credential is missing, malformed, unknown, expired or revoked. Fix or replace the credential. - **403**: the credential is valid but isn't allowed to do this. Change the role, the OAuth scopes, the organization or the token's limits; a new credential of the same kind won't help. | Status | `errorCode` | What to check | | ------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **401** | `UNAUTHENTICATED` | No credential was presented: `x-auth-scheme` or the credential header is missing or misspelled, or an API key was sent as a query parameter | | **401** | `INVALID_CREDENTIAL` | The value is damaged (copy and paste, whitespace), belongs to the other region, or was sent with the wrong scheme; or it no longer exists: a deleted key, a revoked federated token or one whose minting credential is gone, an access token already replaced by a refresh | | **401** | `CREDENTIAL_EXPIRED` | Refresh the OAuth access token, mint a new federated token, or [rotate](https://developer.rhombus.com/authentication/permissions#rotate-an-api-key) a key that reached its `expiresAtMs` | | **401** | `CREDENTIAL_REVOKED` | The OAuth grant was revoked, by the user, an administrator, or automatically after a replaced refresh token was reused. Sign in again | | **401** | `CERTIFICATE_MISMATCH` | `Client certificate does not match the api key`: present the certificate issued for this key, with the scheme `api` (certificate keys work on api2 only) | | **403** | `PERMISSION_REQUIRED` | The key's role ([Permissions](https://developer.rhombus.com/authentication/permissions)). For an OAuth token, also its scopes: a missing scope adds `WWW-Authenticate: Bearer error="insufficient_scope", scope=""` | | **403** | `PARTNER_CLIENT_ACCESS_DENIED` | Your partner account doesn't manage the `x-auth-org` organization | | **403** | `DOMAIN_NOT_ALLOWED` | A federated token minted with a `domain` was used from another origin, or from a client that sends no `Origin` or `Referer` ([The domain field](https://developer.rhombus.com/authentication/federated-tokens#the-domain-field)) | | **403** | `DEVICE_SCOPE_MISMATCH` | A device-scoped federated token was used for a different device | | **400** | `INVALID_AUTH_ORG` | `x-auth-org` isn't a valid organization UUID | | **404** | `NOT_FOUND` | The `/api` path doesn't exist. Paths are case-sensitive, so a wrong capital letter gets 404 too | | **429** | `RATE_LIMITED` | Your organization's rate limit, or a per-camera limit on clips and frames. Wait `Retry-After` seconds ([Rate Limits](https://developer.rhombus.com/rate-limits)) | | **503** | `SERVICE_UNAVAILABLE` | Authentication couldn't be checked. Retry with backoff | Some operations report a refused action with HTTP `200` and `"error": true` in the body, so check the body as well as the status. The [Errors](https://developer.rhombus.com/errors) page has the full catalog of codes and the retry rules. Media hosts and the event WebSocket have their own failure cases; see [Media and streams](https://developer.rhombus.com/authentication/media#troubleshooting) and [WebSocket authentication](https://developer.rhombus.com/websocket/authentication#troubleshooting). ## Next steps - **[API keys and certificates](https://developer.rhombus.com/authentication/api-keys)**: Create a key, choose its role, use mTLS, rotate, renew and revoke - **[Federated tokens](https://developer.rhombus.com/authentication/federated-tokens)**: Short-lived tokens for browsers, mobile apps and cameras on your LAN - **[Media and streams](https://developer.rhombus.com/authentication/media)**: Snapshots, live and recorded video, clips, audio and shared links - **[Permissions and key rotation](https://developer.rhombus.com/authentication/permissions)**: Roles, OAuth scopes, rotation without downtime, and the audit trail --- # API Keys and Certificates Source: https://developer.rhombus.com/authentication/api-keys > Create a Rhombus API key, choose its role, call the API with it, and use a certificate (mTLS) key: expiry, creating, rotating and renewing keys with the API, the CSR, and revocation. An API key is the credential for code you run on your own servers. It belongs to one organization, acts with the **role** you give it, and works until you delete it or until its expiry (`expiresAtMs`), if it has one. A key can be a plain **API token**, sent in a header, or a **certificate** key that also needs a TLS client certificate (mTLS). ## Create an API key 1. In the [Rhombus Console](https://console.rhombussystems.com/settings/integrations/api-tokens/), go to **Settings → Integrations & Developer Resources → API Tokens** and click **Add API Key**. EU organizations use [console.eu.rhombussystems.com](https://console.eu.rhombussystems.com/settings/integrations/api-tokens/). 2. Enter a **Name** that says what uses the key (`warehouse-door-sync`). It shows in the audit log. 3. Choose an **Auth Type**: **Api Token**, or **Certificate** for mTLS (see [Certificate keys](https://developer.rhombus.com/authentication/api-keys#certificate-keys-mtls)). 4. Choose a **Role**. The key can do exactly what the role allows, so pick the narrowest role that covers the job. See [Permissions and key rotation](https://developer.rhombus.com/authentication/permissions). 5. Copy the key. It's shown once. Creating keys needs a role with API administration, and you can only give a key a role you're allowed to assign. To create keys from code, see [Manage keys with the API](https://developer.rhombus.com/authentication/api-keys#manage-keys-with-the-api). ## Call the API with a key Send the scheme and the key as headers on every request: ```bash cURL curl -X POST "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \ -H "x-auth-scheme: api-token" \ -H "x-auth-apikey: $RHOMBUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```python Python import os import requests session = requests.Session() session.headers.update({ "x-auth-scheme": "api-token", "x-auth-apikey": os.environ["RHOMBUS_API_KEY"], "Content-Type": "application/json", }) cameras = session.post( "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList", json={} ).json() ``` ```javascript JavaScript const res = await fetch( "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList", { method: "POST", headers: { "x-auth-scheme": "api-token", "x-auth-apikey": process.env.RHOMBUS_API_KEY, "Content-Type": "application/json", }, body: "{}", }, ); const cameras = await res.json(); ``` The same headers work on the media hosts and the event WebSocket handshake. Partner keys use them too, plus `x-auth-org` to act in a client organization (see [Partner API](https://developer.rhombus.com/partner-api-calls)). The key is never accepted as a query parameter, so it can't authenticate an ``, a `