> ## Documentation index
> Fetch the complete documentation index at: https://developer.rhombus.com/llms.txt
> Use it to discover all available pages before exploring further.

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

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