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

# Face Recognition & People Counting

> Enroll and manage known people, query face-match events, tune the matching threshold, and pull camera people-counting analytics through the Rhombus API.

## Overview

This guide covers two distinct camera analytics capabilities that are easy to confuse:

- **Face Recognition (identity)** — enroll known people from face images, then receive **face-match events** when a camera detects a matching face. Each event carries a confidence score and the best-matching person. Use this to recognize _who_ appears on camera.
- **People counting (headcount)** — aggregate, non-identifying camera analytics that count _how many_ people cross a camera's field of view over time. These come from the Report Webservice as count time series and occupancy counts. No identity is involved.

Through the API you can:

- **Enroll and manage people** by creating person records and uploading face images
- **Query face-match events** with rich filters and pagination
- **Tune the matching threshold** and correct misidentified events
- **Pull people-count analytics** as time series or the most recent counts per camera

> **Note**: **Not the same as occupancy sensors.** Rhombus also offers physical occupancy sensors (Bluetooth/motion hardware) that report room presence. Those are a separate product served by a different API — see the [IoT Sensors](https://developer.rhombus.com/implementations/iot-sensors) guide. This page's "people counting" is _camera_ analytics.

> **Warning**: **Biometric privacy.** Face recognition processes biometric data. Many jurisdictions regulate the collection, storage, and use of biometric identifiers, and some require notice or consent. Review the biometric-privacy laws that apply to your locations before enrolling faces, and confirm your organization's policies. This guide describes only the API surface — it is not legal advice.

## Prerequisites

> **Note**: Before you begin, make sure you have:
>
> - A **Rhombus API key**. Enrolling or managing faces requires **face-management permissions**; reading people-count reports requires report read access. Generate and scope keys in the [Rhombus Console](https://console.rhombussystems.com/settings/integrations/api-tokens/) under **Settings → Integrations & Developer Resources → API Tokens**.
> - At least one **camera** with the relevant analytics enabled (face recognition for identity workflows, people counting for headcount analytics).
> - **Face Recognition is a licensed, gated feature.** If the face endpoints are unavailable for your organization, contact your Rhombus account team to confirm entitlement.

All requests are `POST` to `https://api2.rhombussystems.com` and authenticate with the `x-auth-scheme` and `x-auth-apikey` headers. Timestamps are epoch milliseconds unless noted, and match confidence is a float between `0` and `1`.

## Enroll and manage known people

A **person** is a named identity record. A **matchmaker** is an enrolled face template attached to a person; the recognition engine compares detected faces against your matchmakers.

### Create a person

Create a named person record to enroll against.

```python Python
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/person/createPerson",
    headers=headers,
    json={"name": "Jordan Rivera"}
)

person = response.json().get("person", {})
print(f"Created person {person.get('name')} -> {person.get('uuid')}")
```

```javascript JavaScript
const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/person/createPerson', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-auth-scheme': 'api-token',
    'x-auth-apikey': 'YOUR_API_KEY'
  },
  body: JSON.stringify({ name: 'Jordan Rivera' })
});

const { person } = await response.json();
console.log(`Created person ${person.name} -> ${person.uuid}`);
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/person/createPerson \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{"name": "Jordan Rivera"}'
```

- **`name` (body, string, required)**: Display name for the person.

The response returns a `person` object:

- **`uuid` (string)**: Unique identifier for the person.
- **`name` (string)**: Display name.
- **`email` (string)**: Optional email associated with the person.
- **`orgUuid` (string)**: Organization the person belongs to.
- **`createdOn` (string)**: When the person record was created.
- **`updatedOn` (string)**: When the person record was last updated.

### Upload a face image

Enroll a face by uploading a `.jpg` or `.png` image (maximum **5 MB**) as `multipart/form-data`. The file must be sent under the form field name `file`. Two optional query parameters control the transaction:

- `transaction` — a transaction ID you supply to track and later poll the upload. If omitted, the server generates one and returns it.
- `createPersonIfNotFound` — set to `true` to have the system create a new person automatically when the uploaded face does not match an existing one.

```python Python
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY"
    # Do NOT set Content-Type manually; requests sets the multipart boundary
}

with open("jordan_rivera.jpg", "rb") as f:
    files = {"file": ("jordan_rivera.jpg", f, "image/jpeg")}
    response = requests.post(
        "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers",
        headers=headers,
        params={"transaction": "enroll-jordan-001", "createPersonIfNotFound": "true"},
        files=files
    )

data = response.json()
print(f"Transaction: {data.get('transactionId')}")
for result in data.get("fileUploadResults", []):
    print(f"  {result.get('fileName')}: success={result.get('success')} {result.get('message', '')}")
```

```javascript JavaScript
import fs from 'fs';

const form = new FormData();
const buffer = fs.readFileSync('jordan_rivera.jpg');
form.append('file', new Blob([buffer], { type: 'image/jpeg' }), 'jordan_rivera.jpg');

const url = new URL('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers');
url.searchParams.set('transaction', 'enroll-jordan-001');
url.searchParams.set('createPersonIfNotFound', 'true');

const response = await fetch(url, {
  method: 'POST',
  headers: {
    'x-auth-scheme': 'api-token',
    'x-auth-apikey': 'YOUR_API_KEY'
  },
  body: form
});

const data = await response.json();
console.log(`Transaction: ${data.transactionId}`);
for (const result of data.fileUploadResults || []) {
  console.log(`  ${result.fileName}: success=${result.success} ${result.message || ''}`);
}
```

```bash cURL
curl -X POST "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers?transaction=enroll-jordan-001&createPersonIfNotFound=true" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -F "file=@jordan_rivera.jpg;type=image/jpeg"
```

> **Warning**: Upload one image per request. Send exactly one `file` part, keep it under 5 MB, and use `.jpg` or `.png`. Let your HTTP client set the `Content-Type: multipart/form-data` boundary — do not set it by hand.

The response returns:

- **`transactionId` (string)**: The transaction ID for this upload (echoed back, or generated if you omitted it). Use it to poll processing status.
- **`fileUploadResults` (array)**: Per-file upload results, each with `fileName`, `success` (boolean), and `message`.

### Poll the upload transaction

Face images process asynchronously. Poll the transaction to learn the resulting `faceId` and the `personUuid` the face was enrolled or linked to.

```python Python
import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

for _ in range(10):
    response = requests.post(
        "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction",
        headers=headers,
        json={"transactionId": "enroll-jordan-001"}
    )
    metadata = response.json().get("faceUploadMetadata", [])
    if metadata and all(m.get("success") is not None for m in metadata):
        for m in metadata:
            print(f"faceId={m.get('faceId')} personUuid={m.get('personUuid')} "
                  f"success={m.get('success')} {m.get('errorMsg', '')}")
        break
    time.sleep(2)
```

```javascript JavaScript
const headers = {
  'Content-Type': 'application/json',
  'x-auth-scheme': 'api-token',
  'x-auth-apikey': 'YOUR_API_KEY'
};

for (let i = 0; i < 10; i++) {
  const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction', {
    method: 'POST',
    headers,
    body: JSON.stringify({ transactionId: 'enroll-jordan-001' })
  });
  const { faceUploadMetadata = [] } = await response.json();
  if (faceUploadMetadata.length && faceUploadMetadata.every(m => m.success != null)) {
    for (const m of faceUploadMetadata) {
      console.log(`faceId=${m.faceId} personUuid=${m.personUuid} success=${m.success} ${m.errorMsg || ''}`);
    }
    break;
  }
  await new Promise(r => setTimeout(r, 2000));
}
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{"transactionId": "enroll-jordan-001"}'
```

Each entry in `faceUploadMetadata` includes:

| Field             | Type    | Description                                                 |
| ----------------- | ------- | ----------------------------------------------------------- |
| `transactionId`   | string  | The transaction this upload belongs to                      |
| `faceId`          | string  | Identifier of the enrolled face matchmaker (once processed) |
| `personUuid`      | string  | Person the face was enrolled or linked to                   |
| `success`         | boolean | Whether processing succeeded                                |
| `errorMsg`        | string  | Error detail when `success` is `false`                      |
| `createdAtMillis` | integer | Upload timestamp in epoch milliseconds                      |
| `origS3Key`       | string  | Storage key for the original uploaded image                 |

> **Tip**: Prefer to enroll a person from a face the camera already captured? Use `createFaceMatchmakerFromSighting` with the `faceEventUuid` of an existing face event and the target `personUuid` to turn that sighting into an enrolled template — no upload required.

### Label a person

Attach labels (for example, `employee` or `visitor`) to organize people and filter face events later.

```python Python
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel",
    headers=headers,
    json={"personUuid": "YOUR_PERSON_UUID", "label": "employee"}
)
print(response.json())
```

```javascript JavaScript
const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-auth-scheme': 'api-token',
    'x-auth-apikey': 'YOUR_API_KEY'
  },
  body: JSON.stringify({ personUuid: 'YOUR_PERSON_UUID', label: 'employee' })
});
console.log(await response.json());
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{"personUuid": "YOUR_PERSON_UUID", "label": "employee"}'
```

Use `removePersonLabel` with the same body to detach a label, and `findPersonLabelsByOrg` (empty body `{}`) to retrieve a `labelsByPerson` map of every person's labels.

### List people and matchmakers

List every enrolled person with `findPeopleByOrg`, and list enrolled face templates with `findFaceMatchmakersByOrg` (or `findFaceMatchmakersByPerson` for one person).

```python Python
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

people = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg",
    headers=headers, json={}
).json().get("people", [])

for person in people:
    print(f"{person.get('name')} ({person.get('uuid')})")

matchmakers = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg",
    headers=headers, json={}
).json().get("faceMatchmakers", [])

for mm in matchmakers:
    print(f"faceId={mm.get('id')} personUuid={mm.get('personUuid')} uploaded={mm.get('uploaded')}")
```

```javascript JavaScript
const headers = {
  'Content-Type': 'application/json',
  'x-auth-scheme': 'api-token',
  'x-auth-apikey': 'YOUR_API_KEY'
};

const people = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg', {
  method: 'POST', headers, body: '{}'
})).json();
for (const person of people.people || []) {
  console.log(`${person.name} (${person.uuid})`);
}

const matchmakers = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg', {
  method: 'POST', headers, body: '{}'
})).json();
for (const mm of matchmakers.faceMatchmakers || []) {
  console.log(`faceId=${mm.id} personUuid=${mm.personUuid} uploaded=${mm.uploaded}`);
}
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{}'

curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{}'
```

Each face matchmaker includes `id` (the face ID), `personUuid`, `orgUuid`, `uploaded` (boolean), and `createdOn`.

### Update or remove people and faces

| Endpoint                                                                                                                                                            | Body                                                            | Purpose                          |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------- |
| [`/api/faceRecognition/person/getPerson`](https://developer.rhombus.com/api-reference/face-recognition-person-webservice/get-a-person)                              | `{ "personUuid": "..." }`                                       | Fetch one person                 |
| [`/api/faceRecognition/person/updatePerson`](https://developer.rhombus.com/api-reference/face-recognition-person-webservice/update-person)                          | `{ "personSelectiveUpdate": { "uuid": "...", "name": "..." } }` | Update name/email (selective)    |
| [`/api/faceRecognition/person/deletePerson`](https://developer.rhombus.com/api-reference/face-recognition-person-webservice/delete-person)                          | `{ "personUuid": "..." }`                                       | Delete a person                  |
| [`/api/faceRecognition/matchmaker/getFaceMatchmaker`](https://developer.rhombus.com/api-reference/face-recognition-matchmaker-webservice/get-face-matchmaker)       | `{ "faceId": "..." }`                                           | Fetch one enrolled face          |
| [`/api/faceRecognition/matchmaker/deleteFaceMatchmaker`](https://developer.rhombus.com/api-reference/face-recognition-matchmaker-webservice/delete-face-matchmaker) | `{ "faceId": "..." }`                                           | Remove an enrolled face template |

> **Note**: To manage retention and deletion of biometric data, use `deletePerson` (removes the person and their enrollment) and `deleteFaceMatchmaker` (removes a single enrolled face). Face events can be removed individually with `deleteFaceEvent` (below).

## Query face-match events

When a camera detects a face, it generates a **face event**. Retrieve events with `findFaceEventsByOrg`, which takes a `searchFilter` and a `pageRequest`.

```python Python
import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

now_ms = int(time.time() * 1000)
one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000)

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg",
    headers=headers,
    json={
        "searchFilter": {
            "hasName": True,
            "timestampFilter": {
                "rangeStart": str(one_day_ago_ms),
                "rangeEnd": str(now_ms)
            }
        },
        "pageRequest": {"maxPageSize": 50}
    }
)

data = response.json()
for event in data.get("faceEvents", []):
    match = event.get("selectedPersonMatch") or {}
    print(f"{event.get('faceName')} on {event.get('deviceUuid')} "
          f"confidence={match.get('confidence')} at {event.get('eventTimestamp')}")

# Fetch the next page if lastEvaluatedKey is present
next_key = data.get("lastEvaluatedKey")
```

```javascript JavaScript
const headers = {
  'Content-Type': 'application/json',
  'x-auth-scheme': 'api-token',
  'x-auth-apikey': 'YOUR_API_KEY'
};

const now = Date.now();
const oneDayAgo = now - (24 * 60 * 60 * 1000);

const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    searchFilter: {
      hasName: true,
      timestampFilter: {
        rangeStart: String(oneDayAgo),
        rangeEnd: String(now)
      }
    },
    pageRequest: { maxPageSize: 50 }
  })
});

const data = await response.json();
for (const event of data.faceEvents || []) {
  const match = event.selectedPersonMatch || {};
  console.log(`${event.faceName} on ${event.deviceUuid} confidence=${match.confidence} at ${event.eventTimestamp}`);
}
const nextKey = data.lastEvaluatedKey;
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{
    "searchFilter": {
      "hasName": true,
      "timestampFilter": {
        "rangeStart": "1712620800000",
        "rangeEnd": "1712707200000"
      }
    },
    "pageRequest": { "maxPageSize": 50 }
  }'
```

### Search filter

All `searchFilter` fields are optional. When `timestampFilter` is omitted, the search defaults to the **last 7 days**.

- **`personUuids` (body, string\[])**: Filter to a set of people.
- **`faceNames` (body, string\[])**: Filter by exact person names.
- **`faceNameContains` (body, string)**: Filter by a name substring (minimum 3 characters after trimming). Takes precedence over `faceNames` if both are set.
- **`labels` (body, string\[])**: Filter by person labels.
- **`deviceUuids` (body, string\[])**: Filter to a set of cameras.
- **`locationUuids` (body, string\[])**: Filter to a set of locations.
- **`hasName` (body, boolean)**: Filter by the presence (`true`) or absence (`false`) of a matched person name.
- **`hasEmbedding` (body, boolean)**: Filter by the presence or absence of a face embedding.
- **`timestampFilter` (body, object)**: Time window with `rangeStart` and `rangeEnd`, each a string containing an epoch-millisecond value (inclusive). Defaults to the last 7 days.

### Page request

- **`maxPageSize` (body, integer)**: Maximum number of events to return in one page.
- **`lastEvaluatedKey` (body, string)**: Pagination cursor. Pass the `lastEvaluatedKey` from the previous response to fetch the next page. Absent when there are no more results.

### Interpreting a face event

Each event in `faceEvents` is an object with these fields:

- **`uuid` (string)**: Unique identifier for the face event.
- **`eventTimestamp` (integer)**: When the face was detected, in epoch milliseconds.
- **`faceName` (string)**: Name of the matched person. Equal to the `name` of `selectedPersonMatch` when a match was selected.
- **`personUuid` (string)**: UUID of the matched person, if any.
- **`deviceUuid` (string)**: Camera that generated the event.
- **`locationUuid` (string)**: Location of the camera.
- **`detectionConfidence` (number)**: Confidence (0–1) that the detected image is a face.
- **`embeddingConfidence` (number)**: Confidence (0–1) associated with the generated face signature.
- **`hasEmbedding` (boolean)**: Whether the event has a face signature.
- **`selectedPersonMatch` (object)**: The chosen person match, with `uuid`, `name`, `faceId`, and `confidence` (0–1). Null when no match was selected.
- **`topPersonMatches` (array)**: The best candidate matches for the face, each with `uuid`, `name`, `faceId`, and `confidence` (0–1). Useful for reviewing near-matches.
- **`imageS3Key` (string)**: Storage key for the event image.
- **`thumbnailS3Key` (string)**: Storage key for the event thumbnail.

> **Tip**: Fetch a single event by its UUID with `getFaceEvent` (body `{ "eventUuid": "..." }`).

## Tune matching and correct events

### Read and update the matching threshold

The matching threshold controls how strict face matching is. It is a confidence value between `0` and `1` — a higher value requires a closer match (fewer false positives, more misses); a lower value is more permissive.

```python Python
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

# Read current threshold
current = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig",
    headers=headers, json={}
).json()
print(f"Current threshold: {current.get('faceMatchConfidenceThreshold')}")

# Raise the threshold to reduce false positives
updated = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig",
    headers=headers,
    json={"faceMatchConfidenceThreshold": 0.8}
).json()
print(f"New threshold: {updated.get('faceMatchConfidenceThreshold')}")
```

```javascript JavaScript
const headers = {
  'Content-Type': 'application/json',
  'x-auth-scheme': 'api-token',
  'x-auth-apikey': 'YOUR_API_KEY'
};

const current = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig', {
  method: 'POST', headers, body: '{}'
})).json();
console.log(`Current threshold: ${current.faceMatchConfidenceThreshold}`);

const updated = await (await fetch('https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig', {
  method: 'POST', headers, body: JSON.stringify({ faceMatchConfidenceThreshold: 0.8 })
})).json();
console.log(`New threshold: ${updated.faceMatchConfidenceThreshold}`);
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{}'

curl -X POST https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{"faceMatchConfidenceThreshold": 0.8}'
```

- **`faceMatchConfidenceThreshold` (body, number)**: Minimum match confidence (0–1) required to consider a detected face a match.

### Correct a misidentified event

Reassign a face event to the correct person (or set a name directly) with `updateFaceEvent`. This is how you fix false matches.

```python Python
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent",
    headers=headers,
    json={
        "eventUuid": "YOUR_FACE_EVENT_UUID",
        "personUuid": "CORRECT_PERSON_UUID"
    }
)
print(response.json())
```

```javascript JavaScript
const response = await fetch('https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-auth-scheme': 'api-token',
    'x-auth-apikey': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    eventUuid: 'YOUR_FACE_EVENT_UUID',
    personUuid: 'CORRECT_PERSON_UUID'
  })
});
console.log(await response.json());
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{
    "eventUuid": "YOUR_FACE_EVENT_UUID",
    "personUuid": "CORRECT_PERSON_UUID"
  }'
```

- **`eventUuid` (body, string, required)**: The face event to update.
- **`personUuid` (body, string)**: UUID of the correct person to associate with the event.
- **`personName` (body, string)**: Name to associate with the event. Ignored if `personUuid` matches one of the event's top person matches.

To remove an event entirely, call `deleteFaceEvent` with `{ "eventUuid": "..." }`.

## People-counting analytics

People counting is aggregate camera analytics — it counts people without identifying them, served by the Report Webservice. Use it for footfall, occupancy, and trend dashboards.

### Count time series

`getCountReportV2` returns a time series of counts bucketed by interval. For people counting, set `types` to `["PEOPLE"]`.

```python Python
import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

now_ms = int(time.time() * 1000)
week_ago_ms = now_ms - (7 * 24 * 60 * 60 * 1000)

response = requests.post(
    "https://api2.rhombussystems.com/api/report/getCountReportV2",
    headers=headers,
    json={
        "startTimeMs": week_ago_ms,
        "endTimeMs": now_ms,
        "interval": "DAILY",
        "scope": "DEVICE",
        "types": ["PEOPLE"],
        "uuid": "YOUR_CAMERA_UUID",
        "timeZone": "America/Los_Angeles"
    }
)

data = response.json()
for point in data.get("timeSeriesDataPoints", []):
    counts = point.get("eventCountMap", {})
    print(f"{point.get('dateLocal')}: {counts.get('PEOPLE', 0)} people")
```

```javascript JavaScript
const now = Date.now();
const weekAgo = now - (7 * 24 * 60 * 60 * 1000);

const response = await fetch('https://api2.rhombussystems.com/api/report/getCountReportV2', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-auth-scheme': 'api-token',
    'x-auth-apikey': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    startTimeMs: weekAgo,
    endTimeMs: now,
    interval: 'DAILY',
    scope: 'DEVICE',
    types: ['PEOPLE'],
    uuid: 'YOUR_CAMERA_UUID',
    timeZone: 'America/Los_Angeles'
  })
});

const data = await response.json();
for (const point of data.timeSeriesDataPoints || []) {
  const counts = point.eventCountMap || {};
  console.log(`${point.dateLocal}: ${counts.PEOPLE || 0} people`);
}
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/report/getCountReportV2 \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{
    "startTimeMs": 1712016000000,
    "endTimeMs": 1712620800000,
    "interval": "DAILY",
    "scope": "DEVICE",
    "types": ["PEOPLE"],
    "uuid": "YOUR_CAMERA_UUID",
    "timeZone": "America/Los_Angeles"
  }'
```

- **`startTimeMs` (body, integer, required)**: Start of the range in epoch milliseconds.
- **`endTimeMs` (body, integer, required)**: End of the range in epoch milliseconds.
- **`interval` (body, string, required)**: Bucket size. One of `MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`.
- **`scope` (body, string, required)**: Aggregation scope. One of `REGION`, `DEVICE`, `LOCATION`, `ORG`.
- **`types` (body, string\[], required)**: Report types to include. Use `["PEOPLE"]` for people counting.
- **`uuid` (body, string)**: Target UUID for the chosen scope (for example, a camera UUID when `scope` is `DEVICE`). Omit for `ORG` scope.
- **`timeZone` (body, string)**: IANA time zone used to bucket the data (for example, `America/Los_Angeles`).

Each entry in `timeSeriesDataPoints` includes:

- **`dateUtc` (string)**: Bucket start in UTC.
- **`dateLocal` (string)**: Bucket start in the requested time zone.
- **`eventCountMap` (object)**: Map of report type to count for the bucket (for example, `{ "PEOPLE": 42 }`).
- **`reportingDevicesMap` (object)**: Map describing which devices reported into the bucket.

> **Note**: `getCountReportV2` supports many `types` beyond `PEOPLE` — see the [Reports & Analytics](https://developer.rhombus.com/implementations/reports-analytics) guide for the full report model, additional analytics endpoints, and CSV export.

### Occupancy counts over time

`getOccupancyCountsV2` returns a time series of camera-derived occupancy counts for a single camera.

```python Python
import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

now_ms = int(time.time() * 1000)
day_ago_ms = now_ms - (24 * 60 * 60 * 1000)

response = requests.post(
    "https://api2.rhombussystems.com/api/report/getOccupancyCountsV2",
    headers=headers,
    json={
        "deviceUuid": "YOUR_CAMERA_UUID",
        "startTimeMs": day_ago_ms,
        "endTimeMs": now_ms,
        "interval": "HOURLY"
    }
)

for point in response.json().get("timeSeriesDataPoints", []):
    print(f"{point.get('dateLocal')}: {point.get('eventCountMap')}")
```

```javascript JavaScript
const now = Date.now();
const dayAgo = now - (24 * 60 * 60 * 1000);

const response = await fetch('https://api2.rhombussystems.com/api/report/getOccupancyCountsV2', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-auth-scheme': 'api-token',
    'x-auth-apikey': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    deviceUuid: 'YOUR_CAMERA_UUID',
    startTimeMs: dayAgo,
    endTimeMs: now,
    interval: 'HOURLY'
  })
});

const data = await response.json();
for (const point of data.timeSeriesDataPoints || []) {
  console.log(`${point.dateLocal}:`, point.eventCountMap);
}
```

```bash cURL
curl -X POST https://api2.rhombussystems.com/api/report/getOccupancyCountsV2 \
  -H "Content-Type: application/json" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -d '{
    "deviceUuid": "YOUR_CAMERA_UUID",
    "startTimeMs": 1712534400000,
    "endTimeMs": 1712620800000,
    "interval": "HOURLY"
  }'
```

- **`deviceUuid` (body, string, required)**: Camera to read occupancy counts from.
- **`startTimeMs` (body, integer, required)**: Start of the range in epoch milliseconds.
- **`endTimeMs` (body, integer, required)**: End of the range in epoch milliseconds.
- **`interval` (body, string, required)**: Bucket size (`MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`).

Each data point adds `timestampMs` and `approximateTimestampMsMap` to the standard `dateUtc`, `dateLocal`, `eventCountMap`, and `reportingDevicesMap` fields.

> **Note**: This occupancy report is **camera-derived**. For room presence measured by physical Bluetooth/motion occupancy sensors, see the [IoT Sensors](https://developer.rhombus.com/implementations/iot-sensors) guide — that is a separate hardware product with its own API.

## Reference: report enums

| Enum        | Values                                                                                                               |
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
| Report type | `CROWD`, `PEOPLE`, `FACES`, `MOTION`, `BANDWIDTH`, `VEHICLES`, `LICENSEPLATES`, `ALERTS`, `AM_VERIFICATION`, `DWELL` |
| Interval    | `MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`                                                  |
| Scope       | `REGION`, `DEVICE`, `LOCATION`, `ORG`                                                                                |

## Use cases

- **Watchlist alerting** — enroll people of interest, then poll `findFaceEventsByOrg` filtered by `personUuids` or `labels` to flag sightings.
- **Access reconciliation** — cross-reference face events with access-control activity to confirm the person who badged in matches who the camera saw.
- **Model quality loop** — review `topPersonMatches` on low-confidence events, correct them with `updateFaceEvent`, and tune `faceMatchConfidenceThreshold` to balance false positives against misses.
- **Footfall dashboards** — chart `getCountReportV2` with `types: ["PEOPLE"]` for daily or hourly traffic trends per camera or location.
- **Occupancy tiles** — chart `getOccupancyCountsV2` for a camera to show occupancy through the day.

## Troubleshooting

**Face endpoints return an error or are unavailable**

Face Recognition is a licensed, gated feature. Confirm your organization is entitled and that your API key has face-management permissions. Contact your Rhombus account team if the endpoints are not available.

**uploadFaceMatchmakers rejects my file**

Send exactly one file per request under the form field name `file`, keep it under 5 MB, and use `.jpg` or `.png`. Let your HTTP client set the multipart `Content-Type` boundary — do not set `Content-Type` manually.

**findFaceUploadMetadataByTransaction returns nothing yet**

Uploads process asynchronously. Poll the transaction until each entry reports a `success` value. When `success` is `false`, read `errorMsg` for the reason.

**No face events come back**

If you omit `timestampFilter`, the search only covers the last 7 days. Widen the window with `rangeStart`/`rangeEnd` (epoch-millisecond strings), and loosen filters such as `hasName` or `personUuids`. Remember to page with `lastEvaluatedKey`.

**Too many false matches (or too few matches)**

Adjust `faceMatchConfidenceThreshold` with `updateFaceMatchingConfig`. Raise it (closer to 1) to reduce false positives; lower it to catch more potential matches. Correct individual mistakes with `updateFaceEvent`.

## Next steps

- **[Reports & Analytics](https://developer.rhombus.com/implementations/reports-analytics)**: Full count-report model, threshold-crossing counts, and CSV export
- **[LPR & Vehicle](https://developer.rhombus.com/implementations/lpr-vehicle)**: Detect and search license plates and vehicles across your cameras
- **[IoT Sensors](https://developer.rhombus.com/implementations/iot-sensors)**: Physical occupancy, climate, and environmental sensors
- **[API Reference](https://developer.rhombus.com/api-reference/overview)**: Browse and test every Rhombus API endpoint
