Skip to content

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:

  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:

FieldPresentMeaning
errorAlwaysfalse on success, true when the action failed
errorMsgWhen error is trueA human-readable reason. Log it and show it to an operator; don't parse it, because the wording can change
warningMsgRarelyThe 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:

json
{
  "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:

CodeStatusMeaning
BAD_REQUEST400The request is invalid.
VALIDATION_FAILED400The request failed validation.
MISSING_REQUIRED_FIELD400The request is missing a required field.
INVALID_JSON400The request body is not valid JSON.
INVALID_REQUEST_BODY400The request body does not match the expected object structure.
METHOD_NOT_ALLOWED405This endpoint does not accept that HTTP method; the Allow header lists the ones it does.
UNSUPPORTED_MEDIA_TYPE415This endpoint does not accept that Content-Type. Send application/json.
FEATURE_NOT_ENABLED400This feature is not enabled for the organization.
UNAUTHENTICATED401Authentication is required. Supply valid credentials.
FORBIDDEN403You do not have permission to perform this action.
NOT_FOUND404The requested resource was not found.
CONFLICT409The request conflicts with the current state of the resource.
RATE_LIMITED429Too many requests; try again later.
INTERNAL_ERROR500An unexpected error occurred.
SERVICE_UNAVAILABLE503The service is temporarily unavailable; try again shortly.
REQUEST_FAILED200 with error: trueThe request could not be completed.
INVALID_CREDENTIAL401The supplied credential is malformed or not recognized.
CREDENTIAL_EXPIRED401The supplied credential has expired.
CREDENTIAL_REVOKED401The supplied credential has been revoked.
CERTIFICATE_MISMATCH401The client certificate does not match the supplied credential.
INVALID_AUTH_ORG400The x-auth-org header is not a valid organization UUID.
DOMAIN_NOT_ALLOWED403Requests from this domain are not allowed for this credential.
DEVICE_SCOPE_MISMATCH403This token is scoped to a different device.
ROLE_NOT_PERMITTED403Your role does not permit access to this organization.
FUNCTIONALITY_REQUIRED403Your role is missing a functionality this action requires.
PARTNER_FUNCTIONALITY_REQUIRED403Your partner role is missing a functionality this action requires.
PERMISSION_REQUIRED403Your role does not have the required permission on the requested resource.
PARTNER_CLIENT_ACCESS_DENIED403Your partner organization does not have access to this client organization.
HARDWARE_NOT_AVAILABLE403No hardware with this serial number is available for your organization to register.
HARDWARE_ALREADY_REGISTERED403This hardware is already registered to an organization. Remove it from that organization before registering it again.
LOCATION_NOT_FOUND400The location was not found in the target organization.
PARTNER_PORTAL_NOT_ENABLED403The partner portal is not enabled for your partner organization. Contact Rhombus support.
MANAGED_BY_MSP403This organization is managed by a Managed Service Provider. Contact the provider or Rhombus support.
SUPER_ADMIN_RESTRICTED_BY_MSP403The Super Admin role in this organization is managed by a Managed Service Provider. Contact the provider or Rhombus support.
MSP_DESIGNATION_INVALID403This operation requires an active, signed Managed Service Provider designation.
NOT_MANAGED_CLIENT403This organization is not a managed client of your Managed Service Provider account.
DEVICES_STILL_REGISTERED200 with error: trueDevices are still registered in this client organization.
LICENSES_STILL_ASSIGNED200 with error: trueLicenses you own are still assigned to devices in this client organization.
DOOR_BOUND_HARDWARE200 with error: trueThis device is bound to one or more access-controlled doors. Remove the bindings in the source organization before moving it.
CONFIRM_NAME_MISMATCH409The confirmation text does not match the client organization name.
DUPLICATE_CLIENT_NAME409You already manage a client organization with this name. Set allowDuplicateName to create it anyway.
LICENSE_V2_DISABLED409Pooled licenses cannot be assigned to this client organization because it is not on license entitlements. Contact Rhombus support.
INSUFFICIENT_POOL_LICENSES409Not 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

StatusBodyMeaningRetry?
200 with "error": true{"error":true,"errorMsg":"…", …}The action failed or was refusedNo; act on errorMsg
400{"msg":"Invalid Json", …}The body isn't valid JSONNo
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 acceptNo
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 fieldNo
400{"errorCode":"INVALID_AUTH_ORG", …}The x-auth-org header is malformedNo
401{"errorCode":"UNAUTHENTICATED", …} or another credential codeThe credential is missing, malformed, unknown, expired or revokedNo; see Authentication and permission errors
403{"errorCode":"PERMISSION_REQUIRED", …} or another permission codeThe credential is valid, but it isn't permitted to do thisNo; see 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 existNo
405{"errorCode":"METHOD_NOT_ALLOWED", …}The path doesn't accept this HTTP methodNo
415{"errorCode":"UNSUPPORTED_MEDIA_TYPE", …}The request's Content-Type isn't one the operation acceptsNo
429{"errorCode":"RATE_LIMITED", …}, or an errorMsg naming the camerasYour organization's rate limit, or a per-camera limit on clips and framesYes, after Retry-After; see Rate Limits
500{"msg":"…", …}An unexpected server errorYes, a few times with backoff
503{"errorCode":"SERVICE_UNAVAILABLE", …}Rhombus couldn't check the credential at that momentYes, with backoff
502, 503, 504VariesA temporary gateway or availability problemYes, 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.

StatuserrorCodeCause
401UNAUTHENTICATEDNo credential was presented
401INVALID_CREDENTIALThe credential is malformed or isn't recognized, including a deleted API key or a revoked federated token
401CREDENTIAL_EXPIREDThe credential has expired. Refresh the OAuth access token, mint a new federated token or replace the API key, then retry
401CREDENTIAL_REVOKEDThe OAuth grant behind the access token was revoked
401CERTIFICATE_MISMATCHThe client certificate doesn't belong to the API key (Client certificate does not match the api key)
403PERMISSION_REQUIREDThe credential's role, or an OAuth access token's scopes, don't allow this operation or device
403PARTNER_CLIENT_ACCESS_DENIEDThe partner credential can't act in the organization named in x-auth-org
403DOMAIN_NOT_ALLOWEDA 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
403DEVICE_SCOPE_MISMATCHA 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):

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 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.
  • 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:

ResponseAction
2xx with "error": falseSuccess
2xx with "error": trueDon't retry unchanged; fix the request or the data errorMsg describes
400, 401, 403, 404, 405, 415Don't retry; fix the request, the credential or the role
401 with CREDENTIAL_EXPIREDRenew the credential, then retry
429Wait the Retry-After seconds, then retry
500, 502, 503, 504, a timeout or a dropped connectionRetry with exponential backoff and jitter

How to wait:

  • Throttling (429): the server says how long in Retry-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 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:

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}")

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 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.