Getting Started
Errors and Status Codes
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.
On this page
The REST API reports a failed request in one of two ways, so check both:
- 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": trueand a machine-readableerrorCode. - HTTP
200with"error": truewhen the request was valid but the action failed or was refused, for example a role you're not allowed to assign. The reason is inerrorMsg:
{
"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.
Every error response carries errorCode, a stable code to branch on, next to errorMsg, a message to show or log. Codes are never renamed or removed, but new ones are added: handle a code you don't recognize by its HTTP status.
Every response with an error status has the same body. msg is the HTTP reason phrase, kept for older clients; status repeats the HTTP status; timestamp is when the error happened, in Unix epoch milliseconds; txid is the request ID to quote to support, also sent as the x-rhombus-txid header:
{
"msg": "Forbidden",
"error": true,
"errorCode": "FUNCTIONALITY_REQUIRED",
"errorMsg": "Your role is missing a functionality this action requires.",
"status": 403,
"timestamp": 1791367200000,
"txid": "yJ2kQw8LTaO1xX4hVd7mPA"
}The codes the API returns today:
| Code | Status | Meaning |
|---|---|---|
BAD_REQUEST | 400 | The request is invalid. |
VALIDATION_FAILED | 400 | The request failed validation. |
MISSING_REQUIRED_FIELD | 400 | The request is missing a required field. |
INVALID_JSON | 400 | The request body is not valid JSON. |
INVALID_REQUEST_BODY | 400 | The request body does not match the expected object structure. |
METHOD_NOT_ALLOWED | 405 | This endpoint does not accept that HTTP method; the Allow header lists the ones it does. |
UNSUPPORTED_MEDIA_TYPE | 415 | This endpoint does not accept that Content-Type. Send application/json. |
FEATURE_NOT_ENABLED | 400 | This feature is not enabled for the organization. |
UNAUTHENTICATED | 401 | Authentication is required. Supply valid credentials. |
FORBIDDEN | 403 | You do not have permission to perform this action. |
NOT_FOUND | 404 | The requested resource was not found. |
CONFLICT | 409 | The request conflicts with the current state of the resource. |
RATE_LIMITED | 429 | Too many requests; try again later. |
INTERNAL_ERROR | 500 | An unexpected error occurred. |
SERVICE_UNAVAILABLE | 503 | The service is temporarily unavailable; try again shortly. |
REQUEST_FAILED | 200 with error: | The request could not be completed. |
INVALID_CREDENTIAL | 401 | The supplied credential is malformed or not recognized. |
CREDENTIAL_EXPIRED | 401 | The supplied credential has expired. |
CREDENTIAL_REVOKED | 401 | The supplied credential has been revoked. |
CERTIFICATE_MISMATCH | 401 | The client certificate does not match the supplied credential. |
INVALID_AUTH_ORG | 400 | The x-auth-org header is not a valid organization UUID. |
DOMAIN_NOT_ALLOWED | 403 | Requests from this domain are not allowed for this credential. |
DEVICE_SCOPE_MISMATCH | 403 | This token is scoped to a different device. |
ROLE_NOT_PERMITTED | 403 | Your role does not permit access to this organization. |
FUNCTIONALITY_REQUIRED | 403 | Your role is missing a functionality this action requires. |
PARTNER_FUNCTIONALITY_REQUIRED | 403 | Your partner role is missing a functionality this action requires. |
PERMISSION_REQUIRED | 403 | Your role does not have the required permission on the requested resource. |
PARTNER_CLIENT_ACCESS_DENIED | 403 | Your partner organization does not have access to this client organization. |
HARDWARE_NOT_AVAILABLE | 403 | No hardware with this serial number is available for your organization to register. |
HARDWARE_ALREADY_REGISTERED | 403 | This hardware is already registered to an organization. Remove it from that organization before registering it again. |
LOCATION_NOT_FOUND | 400 | The location was not found in the target organization. |
PARTNER_PORTAL_NOT_ENABLED | 403 | The partner portal is not enabled for your partner organization. Contact Rhombus support. |
MANAGED_BY_MSP | 403 | This organization is managed by a Managed Service Provider. Contact the provider or Rhombus support. |
SUPER_ADMIN_RESTRICTED_BY_MSP | 403 | The Super Admin role in this organization is managed by a Managed Service Provider. Contact the provider or Rhombus support. |
MSP_DESIGNATION_INVALID | 403 | This operation requires an active, signed Managed Service Provider designation. |
NOT_MANAGED_CLIENT | 403 | This organization is not a managed client of your Managed Service Provider account. |
DEVICES_STILL_REGISTERED | 200 with error: | Devices are still registered in this client organization. |
LICENSES_STILL_ASSIGNED | 200 with error: | Licenses you own are still assigned to devices in this client organization. |
DOOR_BOUND_HARDWARE | 200 with error: | This device is bound to one or more access-controlled doors. Remove the bindings in the source organization before moving it. |
CONFIRM_NAME_MISMATCH | 409 | The confirmation text does not match the client organization name. |
DUPLICATE_CLIENT_NAME | 409 | You already manage a client organization with this name. Set allowDuplicateName to create it anyway. |
LICENSE_V2_DISABLED | 409 | Pooled licenses cannot be assigned to this client organization because it is not on license entitlements. Contact Rhombus support. |
INSUFFICIENT_POOL_LICENSES | 409 | Not enough unassigned licenses of this product in the pool. |
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": | {"error":true,"errorMsg":"…", | The action failed or was refused | No; act on errorMsg |
400 | {"msg":"Invalid | The body isn't valid JSON | No |
400 | {"msg":"JSON | 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 , or {"error":true,"errorMsg":"[Error: | 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 |
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 |
404 | {"msg":"Not | 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 |
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 ) |
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):
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 lists what to check for each kind of credential.
Request bodies
- Send
POSTwithContent-Type: application/jsonand 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.
- 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 inSec,SecsorSecondsare 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 totargetUuid.
Retries and backoff
Retry only what can succeed unchanged:
| Response | Action |
|---|---|
2xx with "error": | Success |
2xx with "error": | 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 inRetry-After, for both your organization's limit and the per-camera limits 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 andmin(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,errorMsgandtxid. - 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:
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}")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 support@rhombus.com 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
errorCodeanderrorMsg. - The request ID:
txidin the error body, also sent as thex-rhombus-txidresponse header.
Never send your API key, OAuth tokens or federated tokens.